在 C++98 中实现移动构造函数和移动赋值运算符以获得更好的性能
implement move constructor & move assignment operator in c++98 for better performance
我可以在 C++98 中使用复制构造函数和赋值运算符模拟移动构造函数和移动赋值运算符功能,以提高性能,只要我知道复制构造函数和复制赋值将仅针对代码中的临时对象调用,或者我在我的眼睛里插针吗?
我举了两个例子,一个是普通的复制构造函数和复制赋值运算符,另一个是模拟移动构造函数和移动赋值运算符,并在向量中压入 10000 个元素来调用复制构造函数。
普通复制构造函数和复制赋值运算符的示例(copy.cpp)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(int length)
: _length(length)
, _data(new int[length])
{
}
// Destructor.
~MemoryBlock()
{
if (_data != NULL)
{
// Delete the resource.
delete[] _data;
}
}
//copy constructor.
MemoryBlock(const MemoryBlock& other): _length(other._length)
, _data(new int[other._length])
{
std::copy(other._data, other._data + _length, _data);
}
// copy assignment operator.
MemoryBlock& operator=(MemoryBlock& other)
{
//implementation of copy assignment
}
private:
int _length; // The length of the resource.
int* _data; // The resource.
};
int main()
{
// Create a vector object and add a few elements to it.
vector<MemoryBlock> v;
for(int i=0; i<10000;i++)
v.push_back(MemoryBlock(i));
// Insert a new element into the second position of the vector.
}
带有复制构造函数和复制赋值运算符的模拟移动构造函数和移动赋值运算符功能的示例(move.cpp)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(int length=0)
: _length(length)
, _data(new int[length])
{
}
// Destructor.
~MemoryBlock()
{
if (_data != NULL)
{
// Delete the resource.
delete[] _data;
}
}
// Move constructor.
MemoryBlock(const MemoryBlock& other)
{
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
(const_cast<MemoryBlock&>(other))._data = NULL;
//other._data=NULL;
}
// Move assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
//Implementation of move constructor
return *this;
}
private:
int _length; // The length of the resource.
int* _data; // The resource.
};
int main()
{
// Create a vector object and add a few elements to it.
vector<MemoryBlock> v;
for(int i=0; i<10000;i++)
v.push_back(MemoryBlock(i));
// Insert a new element into the second position of the vector.
}
我观察到性能得到了一些提升:
$ g++ copy.cpp -o copy
$ time ./copy
real 0m0.155s
user 0m0.069s
sys 0m0.085s
$ g++ move.cpp -o move
$ time ./move
real 0m0.023s
user 0m0.013s
sys 0m0.009s
我们可以观察到性能提高了一些成本。
- 在实现移动构造函数和移动赋值时有任何陷阱
c++98 中的运算符模拟功能,即使我确信复制
构造函数和赋值仅在临时对象存在时调用
已创建?
- 有没有其他的way/technique来实现移动构造函数
和 c++98 中的赋值运算符?
您将无法让语言以与 C++11 及更高版本相同的方式理解 R 值,但您仍然可以通过创建自定义 move
语义来近似行为 "R-Value" 模拟所有权转移的类型。
方法
"Move semantics" 实际上只是破坏性地 editing/stealing 对象引用的内容,采用惯用的形式。这与从不可变视图复制到对象相反。在 C++11 及更高版本中在语言级别引入的惯用方法作为重载集呈现给我们,使用左值进行复制 (const T&
),使用(可变的)右值进行移动 (T&&
).
尽管该语言在使用 r 值引用处理生命周期的方式上提供了更深入的钩子,但我们绝对可以通过创建一个rvalue
-like 类型,但会有一些限制。我们所需要的只是一种创建重载集的方法,该重载集可以消除复制概念与移动概念的歧义。
重载集对 C++ 来说并不是什么新鲜事,这可以通过一种薄包装类型来实现,它允许使用基于标记的分派来消除重载的歧义。
例如:
// A type that pretends to be an r-value reference
template <typename T>
class rvalue {
public:
explicit rvalue(T& ref)
: _ref(&ref)
{
}
T& get() const {
return *_ref;
}
operator T&() const {
return *_ref;
}
private:
T* _ref;
};
// returns something that pretends to be an R-value reference
template <typename T>
rvalue<T> move(T& v)
{
return rvalue<T>(v);
}
通过 .
运算符访问成员,我们将无法 完全 像引用一样,因为该功能在 C++ 中不存在——因此具有get()
获取参考。但是我们可以发出一种在代码库中成为惯用的方法来破坏性地改变类型。
rvalue
类型也可以根据您的需要更具创意——为了简洁起见,我只是保持简单。添加 operator->
至少可以直接访问成员可能是值得的。
我省略了 T&&
-> const T&&
转换,T&&
到 U&&
转换(其中 U
是 [=30= 的基础]), 并且 T&&
参考折叠到 T&
。这些东西可以通过使用隐式转换 operators/constructors 修改 rvalue
来引入(但可能需要一些 light-SFINAE)。但是,我发现在泛型编程之外很少需要这样做。对于 pure/basic "move-semantics",这实际上已经足够了。
将它们整合在一起
集成此 "rvalue" 类型就像为 rvalue<T>
添加重载一样简单,其中 T
是 "moved from" 的类型。对于上面的示例,它只需要添加一个构造函数/移动赋值运算符:
// Move constructor.
MemoryBlock(rvalue<MemoryBlock> other)
: _length(other.get()._length),
_data(other.get()._data)
{
other.get()._data = NULL;
}
MoveBlock& operator=(rvalue<MemoryBlock> other)
{
// same idea
}
这使您可以使复制构造函数保持惯用,并模拟 "move" 构造函数。
现在使用可以变成:
MemoryBlock mb(42);
MemoryBlock other = move(mb); // 'move' constructor -- no copy is performed
这是一个比较复制与移动程序集的 working example on compiler explorer。
限制
没有 PR 值可以 rvalue
转化
这种方法的一个显着限制是,您不能 执行 C++11 或更高版本中会发生的 PR 值到 R 值的转换,例如:
MemoryBlock makeMemoryBlock(); // Produces a 'PR-value'
...
// Would be a move in C++11 (if not elided), but would be a copy here
MemoryBlock other = makeMemoryBlock();
据我所知,如果没有语言支持,这是无法复制的。
没有自动生成move-constructors/assignment
与 C++11 不同,不会有自动生成的移动构造函数或赋值运算符——因此对于您想要添加 "move" 支持的类型,这变成了手动工作。
这一点值得指出,因为复制构造函数和赋值运算符在某些情况下是免费提供的,而移动则需要手动完成。
rvalue
不是 L 值参考
在 C++11 中,命名 R 值引用 是 左值引用。这就是为什么您会看到如下代码:
void accept(T&& x)
{
pass_to_something_else(std::move(x));
}
如果没有编译器支持,则无法对这种命名的右值到左值的转换进行建模。这意味着 rvalue
引用将 始终 表现得像 R 值引用。例如:
void accept(rvalue<T> x)
{
pass_to_something_else(x); // still behaves like a 'move'
}
结论
简而言之,您将无法获得对 PR 值等内容的完整语言支持。但是,您至少可以通过 "best-effort" 尝试实现一种允许将内容从一种类型有效地移动到另一种类型的方法。如果这在代码库中得到一致采用,它可以变得像 C++11 及更高版本中的正确移动语义一样惯用。
在我看来,这 "best-effort" 是值得的 尽管有上面列出的限制,因为您仍然可以以惯用的方式更有效地转让所有权。
注意:我不建议重载T&
和const T&
以尝试"move-semantics".这里的大问题是它可能会无意中通过简单的代码变得具有破坏性,例如:
SomeType x; // not-const!
SomeType y = x; // x was moved?
这可能会导致代码中出现错误行为,并且不易被发现。使用包装器方法至少使这种破坏更加明确
我可以在 C++98 中使用复制构造函数和赋值运算符模拟移动构造函数和移动赋值运算符功能,以提高性能,只要我知道复制构造函数和复制赋值将仅针对代码中的临时对象调用,或者我在我的眼睛里插针吗?
我举了两个例子,一个是普通的复制构造函数和复制赋值运算符,另一个是模拟移动构造函数和移动赋值运算符,并在向量中压入 10000 个元素来调用复制构造函数。
普通复制构造函数和复制赋值运算符的示例(copy.cpp)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(int length)
: _length(length)
, _data(new int[length])
{
}
// Destructor.
~MemoryBlock()
{
if (_data != NULL)
{
// Delete the resource.
delete[] _data;
}
}
//copy constructor.
MemoryBlock(const MemoryBlock& other): _length(other._length)
, _data(new int[other._length])
{
std::copy(other._data, other._data + _length, _data);
}
// copy assignment operator.
MemoryBlock& operator=(MemoryBlock& other)
{
//implementation of copy assignment
}
private:
int _length; // The length of the resource.
int* _data; // The resource.
};
int main()
{
// Create a vector object and add a few elements to it.
vector<MemoryBlock> v;
for(int i=0; i<10000;i++)
v.push_back(MemoryBlock(i));
// Insert a new element into the second position of the vector.
}
带有复制构造函数和复制赋值运算符的模拟移动构造函数和移动赋值运算符功能的示例(move.cpp)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(int length=0)
: _length(length)
, _data(new int[length])
{
}
// Destructor.
~MemoryBlock()
{
if (_data != NULL)
{
// Delete the resource.
delete[] _data;
}
}
// Move constructor.
MemoryBlock(const MemoryBlock& other)
{
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
(const_cast<MemoryBlock&>(other))._data = NULL;
//other._data=NULL;
}
// Move assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
//Implementation of move constructor
return *this;
}
private:
int _length; // The length of the resource.
int* _data; // The resource.
};
int main()
{
// Create a vector object and add a few elements to it.
vector<MemoryBlock> v;
for(int i=0; i<10000;i++)
v.push_back(MemoryBlock(i));
// Insert a new element into the second position of the vector.
}
我观察到性能得到了一些提升:
$ g++ copy.cpp -o copy
$ time ./copy
real 0m0.155s
user 0m0.069s
sys 0m0.085s
$ g++ move.cpp -o move
$ time ./move
real 0m0.023s
user 0m0.013s
sys 0m0.009s
我们可以观察到性能提高了一些成本。
- 在实现移动构造函数和移动赋值时有任何陷阱 c++98 中的运算符模拟功能,即使我确信复制 构造函数和赋值仅在临时对象存在时调用 已创建?
- 有没有其他的way/technique来实现移动构造函数 和 c++98 中的赋值运算符?
您将无法让语言以与 C++11 及更高版本相同的方式理解 R 值,但您仍然可以通过创建自定义 move
语义来近似行为 "R-Value" 模拟所有权转移的类型。
方法
"Move semantics" 实际上只是破坏性地 editing/stealing 对象引用的内容,采用惯用的形式。这与从不可变视图复制到对象相反。在 C++11 及更高版本中在语言级别引入的惯用方法作为重载集呈现给我们,使用左值进行复制 (const T&
),使用(可变的)右值进行移动 (T&&
).
尽管该语言在使用 r 值引用处理生命周期的方式上提供了更深入的钩子,但我们绝对可以通过创建一个rvalue
-like 类型,但会有一些限制。我们所需要的只是一种创建重载集的方法,该重载集可以消除复制概念与移动概念的歧义。
重载集对 C++ 来说并不是什么新鲜事,这可以通过一种薄包装类型来实现,它允许使用基于标记的分派来消除重载的歧义。
例如:
// A type that pretends to be an r-value reference
template <typename T>
class rvalue {
public:
explicit rvalue(T& ref)
: _ref(&ref)
{
}
T& get() const {
return *_ref;
}
operator T&() const {
return *_ref;
}
private:
T* _ref;
};
// returns something that pretends to be an R-value reference
template <typename T>
rvalue<T> move(T& v)
{
return rvalue<T>(v);
}
通过 .
运算符访问成员,我们将无法 完全 像引用一样,因为该功能在 C++ 中不存在——因此具有get()
获取参考。但是我们可以发出一种在代码库中成为惯用的方法来破坏性地改变类型。
rvalue
类型也可以根据您的需要更具创意——为了简洁起见,我只是保持简单。添加 operator->
至少可以直接访问成员可能是值得的。
我省略了 T&&
-> const T&&
转换,T&&
到 U&&
转换(其中 U
是 [=30= 的基础]), 并且 T&&
参考折叠到 T&
。这些东西可以通过使用隐式转换 operators/constructors 修改 rvalue
来引入(但可能需要一些 light-SFINAE)。但是,我发现在泛型编程之外很少需要这样做。对于 pure/basic "move-semantics",这实际上已经足够了。
将它们整合在一起
集成此 "rvalue" 类型就像为 rvalue<T>
添加重载一样简单,其中 T
是 "moved from" 的类型。对于上面的示例,它只需要添加一个构造函数/移动赋值运算符:
// Move constructor.
MemoryBlock(rvalue<MemoryBlock> other)
: _length(other.get()._length),
_data(other.get()._data)
{
other.get()._data = NULL;
}
MoveBlock& operator=(rvalue<MemoryBlock> other)
{
// same idea
}
这使您可以使复制构造函数保持惯用,并模拟 "move" 构造函数。
现在使用可以变成:
MemoryBlock mb(42);
MemoryBlock other = move(mb); // 'move' constructor -- no copy is performed
这是一个比较复制与移动程序集的 working example on compiler explorer。
限制
没有 PR 值可以 rvalue
转化
这种方法的一个显着限制是,您不能 执行 C++11 或更高版本中会发生的 PR 值到 R 值的转换,例如:
MemoryBlock makeMemoryBlock(); // Produces a 'PR-value'
...
// Would be a move in C++11 (if not elided), but would be a copy here
MemoryBlock other = makeMemoryBlock();
据我所知,如果没有语言支持,这是无法复制的。
没有自动生成move-constructors/assignment
与 C++11 不同,不会有自动生成的移动构造函数或赋值运算符——因此对于您想要添加 "move" 支持的类型,这变成了手动工作。
这一点值得指出,因为复制构造函数和赋值运算符在某些情况下是免费提供的,而移动则需要手动完成。
rvalue
不是 L 值参考
在 C++11 中,命名 R 值引用 是 左值引用。这就是为什么您会看到如下代码:
void accept(T&& x)
{
pass_to_something_else(std::move(x));
}
如果没有编译器支持,则无法对这种命名的右值到左值的转换进行建模。这意味着 rvalue
引用将 始终 表现得像 R 值引用。例如:
void accept(rvalue<T> x)
{
pass_to_something_else(x); // still behaves like a 'move'
}
结论
简而言之,您将无法获得对 PR 值等内容的完整语言支持。但是,您至少可以通过 "best-effort" 尝试实现一种允许将内容从一种类型有效地移动到另一种类型的方法。如果这在代码库中得到一致采用,它可以变得像 C++11 及更高版本中的正确移动语义一样惯用。
在我看来,这 "best-effort" 是值得的 尽管有上面列出的限制,因为您仍然可以以惯用的方式更有效地转让所有权。
注意:我不建议重载T&
和const T&
以尝试"move-semantics".这里的大问题是它可能会无意中通过简单的代码变得具有破坏性,例如:
SomeType x; // not-const!
SomeType y = x; // x was moved?
这可能会导致代码中出现错误行为,并且不易被发现。使用包装器方法至少使这种破坏更加明确