复制和移动成语?
Copy & Move Idiom?
通过使用 Copy & Swap 惯用语,我们可以轻松实现具有强异常安全性的复制赋值:
T& operator = (T other){
using std::swap;
swap(*this, other);
return *this;
}
然而,这需要 T
为 Swappable. Which a type automatically is if std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true
thanks to std::swap
。
我的问题是,使用 "Copy & Move" 成语有什么缺点吗?像这样:
T& operator = (T other){
*this = std::move(other);
return *this;
}
前提是您为 T
实现了移动赋值,否则显然您最终会遇到无限递归。
此题与Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in C++11? 的不同之处在于,此题更笼统,使用移动赋值运算符而不是实际手动移动成员。这避免了预测链接线程中答案的清理问题。
My question is, is there any downside to using a "Copy & Move" idiom instead?
是的,如果您不实现移动赋值,就会出现堆栈溢出operator =(T&&)
。
如果你确实想实现它,你会得到一个编译器错误 (example here):
struct test
{
test() = default;
test(const test &) = default;
test & operator = (test t)
{
(*this) = std::move(t);
return (*this);
}
test & operator = (test &&)
{
return (*this);
}
};
如果你这样做 test a,b; a = b;
你会得到错误:
error: ambiguous overload for 'operator=' (operand types are 'test' and 'std::remove_reference<test&>::type {aka test}')
解决这个问题的一种方法是使用复制构造函数:
test & operator = (const test& t)
{
*this = std::move(test(t));
return *this;
}
这会起作用,但是如果您不实现移动赋值,您可能不会收到错误(取决于编译器设置)。考虑到人为错误,这种情况可能会发生,并且您最终会在运行时发生堆栈溢出,这很糟糕。
问题更正
实现复制和移动的方法必须像@Raxvan 指出的那样:
T& operator=(const T& other){
*this = T(other);
return *this;
}
但没有 std::move
,因为 T(other)
已经是一个右值,当在这里使用 std::move
时,clang 会发出关于悲观化的警告。
总结
当存在移动赋值运算符时,Copy & Swap 和 Copy & Move 之间的区别取决于用户是否使用 swap
方法,该方法具有比移动赋值更好的异常安全性。对于标准 std::swap
,Copy & Swap 和 Copy & Move 之间的异常安全是相同的。我相信大多数时候,swap
和移动赋值将具有相同的异常安全性(但并非总是如此)。
实施复制和移动存在风险,如果移动赋值运算符不存在或签名错误,复制赋值运算符将减少为无限递归。但是,至少 clang 会警告这一点,并且通过将 -Werror=infinite-recursion
传递给编译器,可以消除这种恐惧,坦率地说,这超出了我的范围,为什么默认情况下这不是错误,但我离题了。
动机
我做了一些测试和大量的摸索,这是我发现的:
如果您有一个移动赋值运算符,"proper" 执行复制和交换的方法将不起作用,因为对 operator=(T)
的调用与 [=23] 不明确=].正如@Raxvan 指出的那样,您需要在复制赋值运算符的主体内部进行复制构造。这被认为是次等的,因为它阻止编译器在使用右值调用运算符时执行复制省略。然而,复制省略的情况现在由移动分配处理,因此这一点没有实际意义。
我们要比较一下:
T& operator=(const T& other){
using std::swap;
swap(*this, T(other));
return *this;
}
至:
T& operator=(const T& other){
*this = T(other);
return *this;
}
如果用户未使用自定义 swap
,则使用模板 std::swap(a,b)
。这基本上是这样做的:
template<typename T>
void swap(T& a, T& b){
T c(std::move(a));
a = std::move(b);
b = std::move(c);
}
这意味着Copy & Swap的异常安全性与move construction和move assignment中较弱的异常安全性相同。如果用户正在使用自定义交换,那么异常安全当然由该交换功能决定。
在复制和移动中,异常安全完全由移动赋值运算符决定。
我认为在这里查看性能有点没有实际意义,因为编译器优化可能会使大多数情况下没有区别。但无论如何我都会评论它,与 Copy & Move 相比,copy 和 swap 执行一个复制构造、一个移动构造和两个移动分配,而 Copy & Move 只执行一个复制构造和一个移动分配。尽管我希望编译器在大多数情况下都能生成相同的机器代码,当然这取决于 T.
附录:我使用的代码
class T {
public:
T() = default;
T(const std::string& n) : name(n) {}
T(const T& other) = default;
#if 0
// Normal Copy & Swap.
//
// Requires this to be Swappable and copy constructible.
//
// Strong exception safety if `std::is_nothrow_swappable_v<T> == true` or user provided
// swap has strong exception safety. Note that if `std::is_nothrow_move_assignable` and
// `std::is_nothrow_move_constructible` are both true, then `std::is_nothrow_swappable`
// is also true but it does not hold that if either of the above are true that T is not
// nothrow swappable as the user may have provided a specialized swap.
//
// Doesn't work in presence of a move assignment operator as T t1 = std::move(t2) becomes
// ambiguous.
T& operator=(T other) {
using std::swap;
swap(*this, other);
return *this;
}
#endif
#if 0
// Copy & Swap in presence of copy-assignment.
//
// Requries this to be Swappable and copy constructible.
//
// Same exception safety as the normal Copy & Swap.
//
// Usually considered inferor to normal Copy & Swap as the compiler now cannot perform
// copy elision when called with an rvalue. However in the presence of a move assignment
// this is moot as any rvalue will bind to the move-assignment instead.
T& operator=(const T& other) {
using std::swap;
swap(*this, T(other));
return *this;
}
#endif
#if 1
// Copy & Move
//
// Requires move-assignment to be implemented and this to be copy constructible.
//
// Exception safety, same as move assignment operator.
//
// If move assignment is not implemented, the assignment to this in the body
// will bind to this function and an infinite recursion will follow.
T& operator=(const T& other) {
// Clang emits the following if a user or default defined move operator is not present.
// > "warning: all paths through this function will call itself [-Winfinite-recursion]"
// I recommend "-Werror=infinite-recursion" or "-Werror" compiler flags to turn this into an
// error.
// This assert will not protect against missing move-assignment operator.
static_assert(std::is_move_assignable<T>::value, "Must be move assignable!");
// Note that the following will cause clang to emit:
// warning: moving a temporary object prevents copy elision [-Wpessimizing-move]
// *this = std::move(T{other});
// The move doesn't do anything anyway so write it like this;
*this = T(other);
return *this;
}
#endif
#if 1
T& operator=(T&& other) {
// This will cause infinite loop if user defined swap is not defined or findable by ADL
// as the templated std::swap will use move assignment.
// using std::swap;
// swap(*this, other);
name = std::move(other.name);
return *this;
}
#endif
private:
std::string name;
};
通过使用 Copy & Swap 惯用语,我们可以轻松实现具有强异常安全性的复制赋值:
T& operator = (T other){
using std::swap;
swap(*this, other);
return *this;
}
然而,这需要 T
为 Swappable. Which a type automatically is if std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true
thanks to std::swap
。
我的问题是,使用 "Copy & Move" 成语有什么缺点吗?像这样:
T& operator = (T other){
*this = std::move(other);
return *this;
}
前提是您为 T
实现了移动赋值,否则显然您最终会遇到无限递归。
此题与Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in C++11? 的不同之处在于,此题更笼统,使用移动赋值运算符而不是实际手动移动成员。这避免了预测链接线程中答案的清理问题。
My question is, is there any downside to using a "Copy & Move" idiom instead?
是的,如果您不实现移动赋值,就会出现堆栈溢出operator =(T&&)
。
如果你确实想实现它,你会得到一个编译器错误 (example here):
struct test
{
test() = default;
test(const test &) = default;
test & operator = (test t)
{
(*this) = std::move(t);
return (*this);
}
test & operator = (test &&)
{
return (*this);
}
};
如果你这样做 test a,b; a = b;
你会得到错误:
error: ambiguous overload for 'operator=' (operand types are 'test' and 'std::remove_reference<test&>::type {aka test}')
解决这个问题的一种方法是使用复制构造函数:
test & operator = (const test& t)
{
*this = std::move(test(t));
return *this;
}
这会起作用,但是如果您不实现移动赋值,您可能不会收到错误(取决于编译器设置)。考虑到人为错误,这种情况可能会发生,并且您最终会在运行时发生堆栈溢出,这很糟糕。
问题更正
实现复制和移动的方法必须像@Raxvan 指出的那样:
T& operator=(const T& other){
*this = T(other);
return *this;
}
但没有 std::move
,因为 T(other)
已经是一个右值,当在这里使用 std::move
时,clang 会发出关于悲观化的警告。
总结
当存在移动赋值运算符时,Copy & Swap 和 Copy & Move 之间的区别取决于用户是否使用 swap
方法,该方法具有比移动赋值更好的异常安全性。对于标准 std::swap
,Copy & Swap 和 Copy & Move 之间的异常安全是相同的。我相信大多数时候,swap
和移动赋值将具有相同的异常安全性(但并非总是如此)。
实施复制和移动存在风险,如果移动赋值运算符不存在或签名错误,复制赋值运算符将减少为无限递归。但是,至少 clang 会警告这一点,并且通过将 -Werror=infinite-recursion
传递给编译器,可以消除这种恐惧,坦率地说,这超出了我的范围,为什么默认情况下这不是错误,但我离题了。
动机
我做了一些测试和大量的摸索,这是我发现的:
如果您有一个移动赋值运算符,"proper" 执行复制和交换的方法将不起作用,因为对
operator=(T)
的调用与 [=23] 不明确=].正如@Raxvan 指出的那样,您需要在复制赋值运算符的主体内部进行复制构造。这被认为是次等的,因为它阻止编译器在使用右值调用运算符时执行复制省略。然而,复制省略的情况现在由移动分配处理,因此这一点没有实际意义。我们要比较一下:
T& operator=(const T& other){ using std::swap; swap(*this, T(other)); return *this; }
至:
T& operator=(const T& other){ *this = T(other); return *this; }
如果用户未使用自定义
swap
,则使用模板std::swap(a,b)
。这基本上是这样做的:template<typename T> void swap(T& a, T& b){ T c(std::move(a)); a = std::move(b); b = std::move(c); }
这意味着Copy & Swap的异常安全性与move construction和move assignment中较弱的异常安全性相同。如果用户正在使用自定义交换,那么异常安全当然由该交换功能决定。
在复制和移动中,异常安全完全由移动赋值运算符决定。
我认为在这里查看性能有点没有实际意义,因为编译器优化可能会使大多数情况下没有区别。但无论如何我都会评论它,与 Copy & Move 相比,copy 和 swap 执行一个复制构造、一个移动构造和两个移动分配,而 Copy & Move 只执行一个复制构造和一个移动分配。尽管我希望编译器在大多数情况下都能生成相同的机器代码,当然这取决于 T.
附录:我使用的代码
class T {
public:
T() = default;
T(const std::string& n) : name(n) {}
T(const T& other) = default;
#if 0
// Normal Copy & Swap.
//
// Requires this to be Swappable and copy constructible.
//
// Strong exception safety if `std::is_nothrow_swappable_v<T> == true` or user provided
// swap has strong exception safety. Note that if `std::is_nothrow_move_assignable` and
// `std::is_nothrow_move_constructible` are both true, then `std::is_nothrow_swappable`
// is also true but it does not hold that if either of the above are true that T is not
// nothrow swappable as the user may have provided a specialized swap.
//
// Doesn't work in presence of a move assignment operator as T t1 = std::move(t2) becomes
// ambiguous.
T& operator=(T other) {
using std::swap;
swap(*this, other);
return *this;
}
#endif
#if 0
// Copy & Swap in presence of copy-assignment.
//
// Requries this to be Swappable and copy constructible.
//
// Same exception safety as the normal Copy & Swap.
//
// Usually considered inferor to normal Copy & Swap as the compiler now cannot perform
// copy elision when called with an rvalue. However in the presence of a move assignment
// this is moot as any rvalue will bind to the move-assignment instead.
T& operator=(const T& other) {
using std::swap;
swap(*this, T(other));
return *this;
}
#endif
#if 1
// Copy & Move
//
// Requires move-assignment to be implemented and this to be copy constructible.
//
// Exception safety, same as move assignment operator.
//
// If move assignment is not implemented, the assignment to this in the body
// will bind to this function and an infinite recursion will follow.
T& operator=(const T& other) {
// Clang emits the following if a user or default defined move operator is not present.
// > "warning: all paths through this function will call itself [-Winfinite-recursion]"
// I recommend "-Werror=infinite-recursion" or "-Werror" compiler flags to turn this into an
// error.
// This assert will not protect against missing move-assignment operator.
static_assert(std::is_move_assignable<T>::value, "Must be move assignable!");
// Note that the following will cause clang to emit:
// warning: moving a temporary object prevents copy elision [-Wpessimizing-move]
// *this = std::move(T{other});
// The move doesn't do anything anyway so write it like this;
*this = T(other);
return *this;
}
#endif
#if 1
T& operator=(T&& other) {
// This will cause infinite loop if user defined swap is not defined or findable by ADL
// as the templated std::swap will use move assignment.
// using std::swap;
// swap(*this, other);
name = std::move(other.name);
return *this;
}
#endif
private:
std::string name;
};