如何实际执行五项规则?
在底部更新
问题1:如何执行五级规则来pipe理相当重的资源,但希望通过价值来传递这些资源,因为这极大地简化和美化了它的用法。 还是不是规则的所有五项甚至需要?
在实践中,我正在开始3D成像,其中图像通常是128 * 128 * 128加倍。 能够写这样的东西会使math更容易:
Data a = MakeData(); Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
q2:使用复制elision / RVO /移动语义的组合,编译器应该能够以最less的复制这个,不是?
我试图找出如何做到这一点,所以我开始了基础知识; 假设一个实现复制和分配的传统方式的对象:
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; };
现在input右值并移动语义。 据我所知,这将是一个工作实施:
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)肯定会修复它,但是我们不会失去一个相当重要的优化机会吗?
除此之外,移动构造函数和赋值的代码显然是重复的。 所以现在我们忘记了含糊不清的问题,并尝试使用复制和交换来解决这个问题,但是现在是rvalues。 正如我们在这里解释的,我们甚至不需要自定义的交换,而是让std :: swap做所有的工作,这听起来很有希望。 所以我写了以下内容,希望std :: swap会使用移动构造函数复制一个临时的,然后用* this来交换:
AnObject& operator = ( AnObject&& rh ) { std::swap( *this, rh ); return *this; }
但是这并不奏效,而是由于std :: swap再次调用了operator =(AnObject && rh),导致堆栈溢出。 问题4:有人可以提供一个例子来说明这个例子的含义吗?
我们可以通过提供第二个交换function解决这个问题:
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; }
现在差不多是代码量的两倍,但是移动部分的代价是很便宜的移动; 但另一方面,正常的作业不能从复制手段中受益。 在这一点上,我真的很困惑,不再看到什么是对的,所以我希望在这里得到一些input。
更新所以看来有两个阵营:
- 一个说跳过移动赋值运算符,继续做C ++ 03教给我们的东西,即写一个赋值运算符来传递参数。
- 另一个说实现移动赋值运算符(毕竟现在是C ++ 11),并且复制赋值运算符通过引用来引用它的参数。
(好吧,第三阵营告诉我要用vector,但是这对于这个假设的类来说已经超出了范围,在现实生活中,我会使用vector,并且还会有其他成员,但是由于移动的构造函数/分配不是自动生成的(但是?)问题仍然存在)
不幸的是我不能在真实世界的场景中testing这两个实现,因为这个项目刚刚开始,数据实际stream向的方式还不得而知。 所以我只是实现了他们两个,增加计数器分配等,并运行了几个迭代约。 这个代码,其中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 >() ); } }
这个代码不够好,不足以testing我之后的情况,或者编译器太聪明了:无论我使用的是arraySize还是numIter,两个阵营的结果几乎相同:分配的数量相同,时间上非常微小的变化,但没有可重复的显着差异。
所以除非有人能指出一个更好的方法来testing这个(鉴于实际的使用范围还不知道),我将不得不得出结论,这并不重要,因此留给开发人员的味道。 在这种情况下,我会select#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
的复制赋值操作符现在只有基本的exception安全性,而不是强大的exception安全性。 不过考虑一下:
您的客户可以随时接受快速分配操作员并赋予强大的exception安全性。 但是他们不能把一个缓慢的赋值运算符加快,
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 /移动语义的组合,编译器应该能够以最less的复制这个,不是?
您可能需要重载您的运营商以利用rvalues中的资源:
Data operator+(Data&& x, const Data& y) { // recycle resources in x! x += y; return std::move(x); }
最终,应该为每个您关心的Data
创build一次资源。 不应该为了移动东西而不必要的new/delete
。
如果你的对象是资源沉重的,你可能想避免完全复制,只提供移动构造函数和移动赋值操作符。 但是,如果你真的想要复制,则很容易提供所有的操作。
你的复制操作看起来很明智,但你的移动操作不。 首先,虽然右值引用参数将绑定到一个右值,在函数内它是一个左值 ,所以你的移动构造函数应该是:
AnObject( AnObject&& rh ) : n( std::move(rh.n) ), a( std::move(rh.a) ) { rh.n = 0; rh.a = nullptr; }
当然,对于像你这样的基本types,实际上并没有什么区别,但也可以养成习惯。
如果你提供了一个移动构造函数,那么当你定义复制分配时,你不需要移动赋值操作符,因为你接受这个参数的值 ,一个右值将被移入参数而不是被复制。
正如你发现的那样,你不能在移动赋值操作符中的整个对象上使用std::swap()
,因为它会recursion到移动赋值操作符中。 您链接到的post中的评论的一点是,如果您提供移动操作,则不需要执行自定义swap
,因为std::swap
将使用您的移动操作。 不幸的是,如果你没有定义一个单独的移动赋值操作符,这不起作用,并且仍然会recursion。 你当然可以使用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没有用户声明的析构函数,并且
移动构造函数不会被隐式定义为删除。
[注意:当移动构造函数没有被隐式声明或显式提供时,否则将调用移动构造函数的expression式可能会调用复制构造函数。 – 注意]
就我个人而言,我对std::vector
更加自信,正确地pipe理它的资源并优化我可以编写的任何代码中的副本/移动。
由于我没有看到其他人明确指出这一点…
你的拷贝赋值操作符如果(并且只有 )通过了一个右值,由于拷贝的影响,所以它的参数值是一个重要的优化机会。 但是在一个赋值运算符的类中, 只有右值(即带有赋值运算符的赋值运算符),这是一个无意义的场景。 所以,模存在其他答案已经指出的内存泄漏,我会说你的类已经是理想的,如果你只是改变复制赋值运算符采取其参数通过const引用。
q3的原始海报
我认为你(和其他响应者)误解了编译器错误的含义,并因此而得出错误的结论。 编译器认为(移动)赋值调用是模糊的,而且是正确的! 你有多个同样合格的方法。
在您的AnObject
类的原始版本中,您的副本构造函数通过const
(左值)引用接收旧对象,而赋值运算符通过(非限定)值接受其参数。 值参数由操作符右侧的任何适当的传递构造函数初始化。 既然你只有一个传递构造函数,那么总是使用这个拷贝构造函数,不pipe原来的右边expression式是左值还是右值。 这使赋值操作符充当复制分配特殊成员函数。
一旦移动构造函数被添加,情况就会改变。 每当赋值运算符被调用时,传输构造函数都有两个select。 复制构造函数将仍然用于左值expression式,但移动构造函数将被用于每当给定右值expression式! 这使得赋值运算符可以同时作为移动赋值特殊成员函数。
当你添加一个传统的移动赋值运算符时,你给了这个类的两个版本的相同的特殊成员函数,这是一个错误。 你已经有了你想要的东西,所以只要摆脱传统的移动赋值操作符,不需要其他的改变。
在你的更新中列出的两个阵营,我想我在技术上是在第一阵营,但由于完全不同的原因。 (不要跳过(传统的)移动赋值操作符,因为它对于你的类是“破”的,但是因为它是多余的。)
顺便说一句,我是新的阅读有关C ++ 11和StackOverflow。 在看到这个问题之前,我从另一个SO问题中得出了这个答案。 ( 更新 :实际上,我仍然打开页面 ,链接转到由FredOverflow显示该技术的特定响应。
关于霍华德·欣南特(Howard Hinnant)2011年5月 – 12日的回应
(我太新手直接评论回应。)
如果以后的testing已经剔除,则不需要明确检查自我分配。 在这种情况下, n != rh.n
已经可以处理大部分了。 但是,如果std::copy
调用超出(当前)内部,那么我们将获得n
组件级自分配。 即使自我分配应该是罕见的,这是由你来决定这些分配是否过于优化。
在委托构造函数的帮助下,你只需要实现每个概念一次;
- 默认初始化
- 资源删除
- 交换
- 复制
其余的只是使用这些。
另外不要忘记做移动赋值(和交换) noexcept
,如果你帮助很多的性能,例如,把你的类放入vector
#include <utility> // header class T { public: T(); T(const T&); T(T&&); T& operator=(const T&); T& operator=(T&&) noexcept; ~T(); void swap(T&) noexcept; private: void copy_from(const T&); }; // implementation T::T() { // here (and only here) you implement default init } T::~T() { // here (and only here) you implement resource delete } void T::swap(T&) noexcept { using std::swap; // enable ADL // here (and only here) you implement swap, typically memberwise swap } void T::copy_from(const T& t) { if( this == &t ) return; // don't forget to protect against self assign // here (and only here) you implement copy } // the rest is generic: T::T(const T& t) : T() { copy_from(t); } T::T(T&& t) : T() { swap(t); } auto T::operator=(const T& t) -> T& { copy_from(t); return *this; } auto T::operator=(T&& t) noexcept -> T& { swap(t); return *this; }