更新
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;
}
现在几乎是金额代码的两倍,但是它的移动部分是通过提供相当便宜的移动来支付的;但另一方面,正常的任务不再受益于复制省略。 在这一点上,我真的很困惑,并没有再看到什么是对错,所以我希望在这里得到一些意见..
更新所以似乎有两个阵营:
(好吧,第3阵营告诉我使用矢量,但这有点超出了这个假设类的范围。在现实生活中我会使用矢量,并且还会有其他成员,但是因为移动构造函数/赋值不会自动生成(但是?)问题仍然存在)
不幸的是,我无法在现实世界的场景中测试这两种实现,因为这个项目刚刚开始,数据实际流动的方式还不知道。所以我简单地实现了它们,添加了分配计数器等,并运行了大约几次迭代。这段代码,其中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。
答案 0 :(得分:17)
您错过了复制赋值运算符的重要优化。随后情况变得混乱。
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:使用copy elision / RVO / move语义的组合 编译器应该可以这个 最少复制,没有?
您可能需要重载运算符以利用rvalues中的资源:
Data operator+(Data&& x, const Data& y)
{
// recycle resources in x!
x += y;
return std::move(x);
}
最终,对于您关心的每个Data
,应该只创建一次资源。应该没有不必要的new/delete
只是为了移动东西。
答案 1 :(得分:13)
如果您的对象资源很多,您可能希望完全避免复制,只需提供移动构造函数和移动赋值运算符。但是,如果您真的想要复制,则很容易提供所有操作。
您的复制操作看起来很合理,但您的移动操作却没有。首先,虽然右值参考参数绑定到右值,但在函数内它是左值,所以你的移动构造函数应该是:
AnObject( AnObject&& rh ) :
n( std::move(rh.n) ),
a( std::move(rh.a) )
{
rh.n = 0;
rh.a = nullptr;
}
当然,对于像你这样的基本类型来说它实际上并没有什么区别,但是养成习惯也是如此。
如果你提供了一个移动构造函数,那么在你定义复制赋值时就不需要一个移动赋值操作符了 - 因为你接受 value 的参数, rvalue将被移入参数而不是复制。
如您所见,您不能在移动赋值运算符内的整个对象上使用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;
};
答案 2 :(得分:4)
让我帮助你:
#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
正确管理其资源并优化我可以编写的任何代码中的副本/移动更有信心。
答案 3 :(得分:1)
因为我还没有看到其他人明确指出这一点......
如果(并且仅如果)由于复制省略而传递了右值,则您的副本赋值运算符按值获取其参数是一个重要的优化机会。但是在具有赋值运算符的类中,显式仅采用rvalues(即具有移动赋值运算符的rvalues),这是一个荒谬的场景。因此,模拟已经在其他答案中指出的内存泄漏,我会说如果您只是更改复制赋值运算符以通过const引用获取其参数,那么您的类已经很理想了。
答案 4 :(得分:1)
原始海报的q3
我认为你(以及其他一些响应者)误解了编译器错误的含义,并因此得出了错误的结论。编译器认为(移动)赋值调用是不明确的,它是正确的!您有多种方法同样合格。
在原始版本的AnObject
类中,复制构造函数通过const
(左值)引用接受旧对象,而赋值运算符通过(非限定)值获取其参数。 value参数由适当的传递构造函数从运算符右侧的任何内容初始化。由于您只有一个传输构造函数,因此无论原始右侧表达式是左值还是右值,都始终使用该复制构造函数。这使赋值运算符充当复制赋值特殊成员函数。
添加移动构造函数后,情况会发生变化。每当调用赋值运算符时,传输构造函数有两种选择。复制构造函数仍将用于左值表达式,但只要给出右值表达式,就会使用移动构造函数!这使赋值运算符同时充当移动赋值特殊成员函数。
当您添加传统的移动赋值运算符时,您为该类提供了相同特殊成员函数的两个版本,这是一个错误。你已经拥有了你想要的东西,所以只需要摆脱传统的移动赋值算子,不需要进行任何其他改动。
在您的更新中列出的两个阵营中,我想我在技术上处于第一阵营,但原因完全不同。 (不要跳过(传统的)移动赋值运算符,因为它对于你的类来说是“破碎的”,但是因为它是多余的。)
顺便说一下,我刚开始阅读有关C ++ 11和StackOverflow的内容。我通过浏览另一个S.O.得出了这个答案。在看到这个之前的问题。 (更新:实际上,我仍然打开page。链接转到显示该技术的FredOverflow的特定响应。)
关于2011年5月12日Howard Hinnant的回复
(我是一个直接评论回复的新手。)
如果稍后的测试已经剔除它,则不需要显式检查自我分配。在这种情况下,n != rh.n
已经完成了大部分工作。但是,std::copy
调用超出了(当前)内部if
,因此我们将获得n
组件级别的自我分配。由你来决定这些作业是否过于反对,即使自我任命应该是罕见的。
答案 5 :(得分:1)
在委托构造函数的帮助下,您只需要实现一次概念;
其他人只是使用那些。
也不要忘记进行移动分配(和交换)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;
}
DataGridView