如何实际执行五项规则?
在底部更新
问题1:如何为一个管理相当重的资源的类实施五项规则,但是您希望它按价值传递,因为这极大地简化和美化了它的用法。 或者不是规则的全部五项甚至需要?
在实践中,我开始使用3D成像,其中图像通常是128 * 128 * 128加倍。 尽管写这样的东西会使数学变得更容易:
Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
q2:使用复制elision / RVO /移动语义的组合,编译器应该能够以最少的复制来实现这一点,不是吗?
我试图弄清楚如何做到这一点,所以我开始了基础知识; 假设一个实现复制和分配的传统方法的对象:
class AnObject
{
public:
AnObject( size_t n = 0 ) :
n( n ),
a( new int[ n ] )
{}
AnObject( const AnObject& rh ) :
n( rh.n ),
a( new int[ rh.n ] )
{
std::copy( rh.a, rh.a + n, a );
}
AnObject& operator = ( AnObject rh )
{
swap( *this, rh );
return *this;
}
friend void swap( AnObject& first, AnObject& second )
{
std::swap( first.n, second.n );
std::swap( first.a, second.a );
}
~AnObject()
{
delete [] a;
}
private:
size_t n;
int* a;
};
现在输入右值并移动语义。 据我所知,这将是一个有效的实施:
AnObject( AnObject&& rh ) :
n( rh.n ),
a( rh.a )
{
rh.n = 0;
rh.a = nullptr;
}
AnObject& operator = ( AnObject&& rh )
{
n = rh.n;
a = rh.a;
rh.n = 0;
rh.a = nullptr;
return *this;
}
然而,编译器(VC ++ 2010 SP1)对此并不满意,编译器通常是正确的:
AnObject make()
{
return AnObject();
}
int main()
{
AnObject a;
a = make(); //error C2593: 'operator =' is ambiguous
}
问题3:如何解决这个问题? 回到AnObject&operator =(const AnObject&rh)当然可以修复它,但是我们不会失去一个相当重要的优化机会吗?
除此之外,很明显移动构造函数和赋值的代码充满了重复。 因此,现在我们忘记了含糊不清,并尝试使用复制和交换来解决这个问题,但是现在对于右值。 正如这里所解释的,我们甚至不需要定制交换,而是使用std :: swap完成所有工作,这听起来非常有前途。 所以我写了以下内容,希望std :: swap会使用移动构造函数复制构建一个临时文件,然后将其与* this进行交换:
AnObject& operator = ( AnObject&& rh )
{
std::swap( *this, rh );
return *this;
}
但是,这并不奏效,而是由于std :: swap再次调用我们的operator =(AnObject && rh),导致堆栈溢出,因为无限递归。 Q4:有人可以举例说明那个例子中的含义吗?
我们可以通过提供第二个交换功能来解决此问题:
AnObject( AnObject&& rh )
{
swap( *this, std::move( rh ) );
}
AnObject& operator = ( AnObject&& rh )
{
swap( *this, std::move( rh ) );
return *this;
}
friend void swap( AnObject& first, AnObject&& second )
{
first.n = second.n;
first.a = second.a;
second.n = 0;
second.a = nullptr;
}
现在差不多是代码量的两倍,但是它的移动部分通过降低相当便宜的移动来支付; 但另一方面,正常的作业不再受益于复制手段。 在这一点上,我很困惑,而且再也看不到什么是非对的,所以我希望在这里得到一些输入。
更新所以看起来有两个阵营:
(好吧,第三阵营告诉我要使用矢量,但是这对于这个假设的类来说有点不合适,在现实生活中,我会使用矢量,并且还会有其他成员,但由于移动构造函数/分配不会自动生成(但?)问题仍然存在)
不幸的是,我不能在真实世界的场景中测试这两个实现,因为这个项目刚刚开始,数据实际流向的方式还不得而知。 所以我简单地实现了他们两个,增加了分配等计数器,并且运行了几次迭代。 这个代码,其中T是其中一个实现:
template< class T >
T make() { return T( narraySize ); }
template< class T >
void assign( T& r ) { r = make< T >(); }
template< class T >
void Test()
{
T a;
T b;
for( size_t i = 0 ; i < numIter ; ++i )
{
assign( a );
assign( b );
T d( a );
T e( b );
T f( make< T >() );
T g( make< T >() + make< T >() );
}
}
这个代码不够好,不足以测试我之后的事情,或者编译器太聪明了:无论我使用的是arraySize和numIter,这两个阵营的结果几乎完全相同:分配的数量相同,时间上非常微小的变化,但没有可再现的显着差异。
所以除非有人能指出一个更好的方法来测试这个(鉴于实际的使用范围还不知道),我将不得不得出结论,这并不重要,因此留给开发者的味道。 在这种情况下,我会选择#2。
您错过了复印分配操作员的重大优化。 随后情况变得混乱。
AnObject& operator = ( const AnObject& rh )
{
if (this != &rh)
{
if (n != rh.n)
{
delete [] a;
n = 0;
a = new int [ rh.n ];
n = rh.n;
}
std::copy(rh.a, rh.a+n, a);
}
return *this;
}
除非你真的永远不会认为你会分配相同大小的AnObject
这会更好。 永远不要扔掉资源,如果你可以回收它们。
有些人可能会抱怨AnObject
的复制赋值操作符现在只具有基本的异常安全性,而不是强大的异常安全性。 不过考虑一下:
您的客户始终可以快速分配操作员,并赋予其强大的异常安全性。 但他们不能采用一个缓慢的赋值运算符并使其更快。
template <class T>
T&
strong_assign(T& x, T y)
{
swap(x, y);
return x;
}
移动构造函数没问题,但移动赋值运算符有内存泄漏。 它应该是:
AnObject& operator = ( AnObject&& rh )
{
delete [] a;
n = rh.n;
a = rh.a;
rh.n = 0;
rh.a = nullptr;
return *this;
}
...
Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
q2:使用复制elision / RVO /移动语义的组合,编译器应该能够以最少的复制来实现这一点,不是吗?
您可能需要重载您的运营商以利用rvalues中的资源:
Data operator+(Data&& x, const Data& y)
{
// recycle resources in x!
x += y;
return std::move(x);
}
最终,应该为您关心的每个Data
创建一次资源。 应该没有不必要的new/delete
只是为了移动东西。
如果你的对象资源很大,你可能希望避免完全拷贝,只提供移动构造函数和移动赋值操作符。 但是,如果您真的想要复制,则很容易提供所有操作。
您的复制操作看起来很明智,但您的移动操作不会。 首先,虽然右值引用参数会绑定到右值,但在函数内它是左值 ,所以您的移动构造函数应该是:
AnObject( AnObject&& rh ) :
n( std::move(rh.n) ),
a( std::move(rh.a) )
{
rh.n = 0;
rh.a = nullptr;
}
当然,对于像你这样的基本类型,其实并没有什么区别,但也应该养成这种习惯。
如果您提供移动构造函数,那么当您定义复制分配时,您不需要移动赋值运算符,因为您接受按值的参数,右值将移入参数而不是复制。
正如你发现的那样,你不能在移动赋值操作符中的整个对象上使用std::swap()
,因为它会递归回移位赋值操作符。 您链接到的帖子中的评论意见是,如果您提供移动操作,则不需要实施自定义swap
,因为std::swap
将使用移动操作。 不幸的是,如果你没有定义一个单独的移动赋值操作符,这不起作用,并且仍然会递归。 你当然可以使用std::swap
交换成员:
AnObject& operator=(AnObject other)
{
std::swap(n,other.n);
std::swap(a,other.a);
return *this;
}
因此你的最后一堂课是:
class AnObject
{
public:
AnObject( size_t n = 0 ) :
n( n ),
a( new int[ n ] )
{}
AnObject( const AnObject& rh ) :
n( rh.n ),
a( new int[ rh.n ] )
{
std::copy( rh.a, rh.a + n, a );
}
AnObject( AnObject&& rh ) :
n( std::move(rh.n) ),
a( std::move(rh.a) )
{
rh.n = 0;
rh.a = nullptr;
}
AnObject& operator = ( AnObject rh )
{
std::swap(n,rh.n);
std::swap(a,rh.a);
return *this;
}
~AnObject()
{
delete [] a;
}
private:
size_t n;
int* a;
};
我来帮你:
#include <vector>
class AnObject
{
public:
AnObject( size_t n = 0 ) : data(n) {}
private:
std::vector<int> data;
};
从C ++ 0x FDIS, [class.copy]注9:
如果类X的定义没有明确声明移动构造函数,则当且仅当
X没有用户声明的拷贝构造函数,
X没有用户声明的复制赋值操作符,
X没有用户声明的移动赋值运算符,
X没有用户声明的析构函数,并且
移动构造函数不会被隐式定义为删除。
[注意:当移动构造函数未被隐式声明或显式提供时,否则将调用移动构造函数的表达式可能会调用复制构造函数。 - 注意]
就我个人而言,我对std::vector
更加自信,正确地管理其资源并优化我可以编写的任何代码中的副本/移动。
上一篇: How to actually implement the rule of five?
下一篇: C++11 rvalues and move semantics confusion (return statement)