UPDATE 在底部
q1: 如何为管理相当繁重的资源的类实现rule of five,但是您希望它按值传递,因为这极大地简化并美化了它的用法?或者甚至不需要所有五项规则?
在实践中,我开始使用3D成像,其中图像通常是128 * 128 * 128双倍 . 能够写这样的东西会让数学变得更容易:
Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
q2: 使用复制省略/ 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;
};
现在输入rvalues并移动语义 . 据我所知,这将是一个有效的实施:
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
}
q3: 如何解决这个问题?回到AnObject&operator =(const AnObject&rh)肯定会修复它,但是我们不会失去一个相当重要的优化机会吗?
除此之外,很明显移动构造函数和赋值的代码充满了重复 . 所以现在我们忘记了歧义,并尝试使用复制和交换来解决这个问题,但现在我们用于rvalues . 正如here解释的那样,我们甚至不需要自定义交换,而是让std :: swap完成所有工作,这听起来很有希望 . 所以我写了以下内容,希望std :: swap将使用move构造函数复制构造一个临时构建器,然后用* 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;
}
现在几乎是金额代码的两倍,但是它的移动部分是通过提供相当便宜的移动来支付的;但另一方面,正常的任务不再受益于复制省略 . 在这一点上,我真的很困惑,并没有看到什么是对的,所以我希望在这里得到一些输入..
UPDATE 所以似乎有两个阵营:
-
有人说要跳过移动赋值运算符并继续执行C 03教给我们的东西,即写一个按值传递参数的赋值运算符 .
-
另一个人说要实现移动赋值运算符(毕竟,它现在是C 11)并让复制赋值运算符通过引用获取其参数 .
(好吧,第三阵营告诉我使用一个向量,但这有点超出了这个假设类的范围 . 在现实生活中我会使用一个向量,并且还会有其他成员,但是因为移动构造函数/分配不是自动生成的(但是?)问题仍然存在)
遗憾的是,由于该项目刚刚开始并且数据实际流动的方式尚不清楚,因此无法在实际场景中测试这两种实现 . 所以我简单地实现了它们,添加了分配计数器等,并运行了大约几次迭代 . 这段代码,其中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使用什么都没关系,两个阵营的结果几乎相同:相同的分配数量,时间上的微小变化,但没有可重现的显着差异 .
因此,除非有人可以指出一种更好的方法来测试这个(假设实际使用的scnearios尚未知晓),我将不得不得出结论,它无关紧要,因此留给开发者的味道 . 在这种情况下,我会选择#2 .
6 回答
您错过了复制赋值运算符的重要优化 . 随后情况变得混乱 .
除非你真的从不认为你将分配相同大小的
AnObject
,否则这要好得多 . 如果可以回收资源,切勿丢弃资源 .有些人可能会抱怨
AnObject
的复制赋值操作符现在只具有基本的异常安全性,而不是强大的异常安全性 . 但考虑一下:您的移动构造函数很好,但您的移动赋值运算符有内存泄漏 . 它应该是:
...
您可能需要重载运算符以利用rvalues中的资源:
最终,资源应该为您关心的每一个创建一次 . 应该没有不必要的
new/delete
只是为了移动东西 .如果您的对象资源很多,您可能希望完全避免复制,只需提供移动构造函数和移动赋值运算符 . 但是,如果您真的想要复制,则很容易提供所有操作 .
您的复制操作看起来很合理,但您的移动操作却没有 . 首先,虽然rvalue引用参数将 bind 到rvalue,但在函数内它是 lvalue ,所以你的移动构造函数应该是:
当然,对于你已经到过这里的基本类型,它实际上并没有什么不同,但是养成习惯也是如此 .
如果你提供了一个移动构造函数,那么在你定义复制赋值时就不需要一个移动赋值操作符了 - 因为你接受了 value 的参数,一个右值将被移入参数而不是被复制 .
如您所见,您不能在移动赋值运算符内的整个对象上使用
std::swap()
,因为它将递归回移动赋值运算符 . 您链接到的帖子中的注释点是,如果您提供移动操作,则不需要实现自定义swap
,因为std::swap
将使用您的移动操作 . 不幸的是,如果你没有工作,仍然会递归 . 您当然可以使用std::swap
来交换成员:因此,你的最后一堂课是:
让我来帮助你:
从C 0x FDIS, [class.copy] 注9:
就个人而言,我更有信心在
std::vector
正确管理其资源并优化我可以编写的任何代码中的副本/移动 .既然我还没有看到其他人明确指出这一点......
由于复制省略,如果(和 only 如果)它传递了一个右值,那么您的副本赋值运算符按值获取其参数是一个重要的优化机会 . 但是在具有赋值运算符的类中,显式 only 采用rvalues(即,具有移动赋值运算符的一个),这是一个荒谬的场景 . 因此,模拟已经在其他答案中指出的内存泄漏,我会说如果你只是改变复制赋值运算符以通过const引用获取其参数,那么你的类已经很理想了 .
q3 of the Original Poster
我认为你(以及其他一些响应者)误解了编译器错误的含义,并因此得出了错误的结论 . 编译器认为(移动)赋值调用是不明确的,它是正确的!您有多个同等资格的方法 .
在原始版本的
AnObject
类中,复制构造函数通过const
(左值)引用接受旧对象,而赋值运算符通过(非限定)值获取其参数 . value参数由适当的传递构造函数从运算符右侧的任何内容初始化 . 由于您只有一个传输构造函数,因此无论原始右侧表达式是左值还是右值,都始终使用该复制构造函数 . 这使赋值运算符充当复制赋值特殊成员函数 .一旦搬家,情况就会发生变化构造函数已添加 . 每当调用赋值运算符时,传输构造函数有两种选择 . 复制构造函数仍将用于左值表达式,但只要给出右值表达式,就会使用移动构造函数!这使赋值运算符同时充当移动赋值特殊成员函数 .
当您添加传统的移动赋值运算符时,您为该类提供了相同特殊成员函数的两个版本,这是一个错误 . 你已经拥有了你想要的东西,所以只需要摆脱传统的移动赋值运算符,而不需要进行其他更改 .
在您的更新中列出的两个阵营中,我想我在技术上处于第一阵营,但原因完全不同 . (不要跳过(传统的)移动赋值运算符,因为它对于你的类来说是“破碎的”,但是因为它是多余的 . )
顺便说一句,我是读新的C 11和StackOverflow的新手 . 我在浏览另一个S.O.时想出了这个答案 . 在看到这个之前的问题 . ( Update :实际上,我仍然打开page . 链接转到FredOverflow的特定响应,显示了该技术 . )
About the 2011-May-12 Response by Howard Hinnant
(我是一个直接评论回答的新手 . )
如果稍后的测试已经剔除它,则不需要显式检查自我赋值 . 在这种情况下,
n != rh.n
已经完成了大部分工作 . 但是,std::copy
调用在(当前)内部if
之外,因此我们将获得n
组件级别的自我分配 . 由您来决定这些作业是否过于反向,即使自我指派应该很少 .在委托构造函数的帮助下,您只需要实现一次每个概念;
默认初始化
资源删除
交换
副本
其余的只是使用那些 .
另外不要忘记进行移动分配(和交换)
noexcept
,如果你有很多帮助,例如,你把你的 class 放在vector
中