By using the Copy & Swap idiom we can easily implement copy assignment with strong exception safety:
T& operator = (T other){
using std::swap;
swap(*this, other);
return *this;
}
However this requires T
to be Swappable. Which a type automatically is if std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true
thanks to std::swap
.
My question is, is there any downside to using a "Copy & Move" idiom instead? Like so:
T& operator = (T other){
*this = std::move(other);
return *this;
}
provided that you implement move-assignment for T
because obviously you end up with infinite recursion otherwise.
This question is different from Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in C++11? in that this question is more general and uses the move assignment operator instead of actually moving the members manually. Which avoids the issues with clean-up that predicted the answer in the linked thread.
The way to implement Copy & Move has to be as @Raxvan pointed out:
T& operator=(const T& other){
*this = T(other);
return *this;
}
but without the std::move
as T(other)
already is an rvalue and clang will emit a warning about pessimisation when using std::move
here.
When a move assignment operator exists, the difference between Copy & Swap and Copy & Move is dependent on whether the user is using a swap
method which has better exception safety than the move assignment. For the standard std::swap
the exception safety is identical between Copy & Swap and Copy & Move. I believe that most of the time, it will be the case that swap
and the move assignment will have the same exception safety (but not always).
Implementing Copy & Move has a risk where if the move assignment operator isn't present or has the wrong signature, the copy assignment operator will reduce to infinite recursion. However at least clang warns about this and by passing -Werror=infinite-recursion
to the compiler this fear can be removed, which quite frankly is beyond me why that is not an error by default, but I digress.
I have done some testing and a lot of head scratching and here is what I have found out:
If you have a move assignment operator, the "proper" way of doing Copy & Swap won't work due to the call to operator=(T)
being ambiguous with operator=(T&&)
. As @Raxvan pointed out, you need to do the copy construction inside of the body of the copy assignment operator. This is considered inferior as it prevents the compiler from performing copy elision when the operator is called with an rvalue. However the cases where copy elision would have applied are handled by the move assignment now so that point is moot.
We have to compare:
T& operator=(const T& other){
using std::swap;
swap(*this, T(other));
return *this;
}
to:
T& operator=(const T& other){
*this = T(other);
return *this;
}
If the user isn't using a custom swap
, then the templated std::swap(a,b)
is used. Which essentially does this:
template<typename T>
void swap(T& a, T& b){
T c(std::move(a));
a = std::move(b);
b = std::move(c);
}
Which means that the exception safety of Copy & Swap is the same exception safety as the weaker of move construction and move assignment. If the user is using a custom swap, then of course the exception safety is dictated by that swap function.
In the Copy & Move, the exception safety is dictated entirely by the move assignment operator.
I believe that looking at performance here is kind of moot as compiler optimizations will likely make there be no difference in most cases. But I'll remark on it anyway the copy and swap performs a copy construction, a move construction and two move assignments, compared to Copy & Move which does a copy construction and only one move assignment. Although I'm kind of expecting the compiler to crank out the same machine code in most cases, of course depending on 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;
};