Typically, given some type T
, to implement copy and move assignment, one needs two functions
T& operator=(T&&) { ... }
T& operator=(const T&) { ... }
Recently, I come to realize that a single one is just sufficient
T& operator=(T v) {
swap(v);
return *this;
}
This version takes advantage of the copy/move constructor. Whether the assignment is copy or move depends on how v
is constructed. This version may even be faster than the first one, since pass-by-value allows for more space for compiler optimization [1]. So, what is the advantage of the first version over the second one that even the standard library uses it?
[1] I guess this explains why tag and function objects are passed by value in the standard library.
The subtle difference is, if you create with a copy or move semantic a new object based on an existing one, that the copy semantic will copy the elements of the resource, that the move semantic will move the elements of the resource. Of course, copying is expensive, moving is cheap.
The move assignment operator is different than a move constructor because a move assignment operator is called on an existing object, while a move constructor is called on an object created by the operation. Thereafter, the other object's data is no longer valid.
Move constructor moves the resources in the heap, i.e., unlike copy constructors which copy the data of the existing object and assigning it to the new object move constructor just makes the pointer of the declared object to point to the data of temporary object and nulls out the pointer of the temporary objects.
std::swap
is implemented by performing move construction followed by two move assignment operations. So unless you implement your own swap
operation that replaces the standard-provided one, your code as presented is an infinite loop.
So you can either implement 2 operator=
methods, or implement one operator=
method and one swap
method. In terms of the number of functions called, it's ultimately identical.
Furthermore, your version of operator=
is sometimes less efficient. Unless the construction of the parameter is elided, that construction will be done via a copy/move from the caller's value. Following this are 1 move construction and 2 move assignments (or whatever your swap
does). Whereas proper operator=
overloads can work directly with the reference it is given.
And this assumes that you cannot write an optimal version of actual assignment. Consider copy-assigning one vector
to another. If the destination vector
has enough storage to hold the size of the source vector... you don't need to allocate. Whereas if you copy construct, you must allocate storage. Only to then free the storage you could have used.
Even in the best case scenario, your copy/move&swap will be no more efficient than using a value. After all, you're going to take a reference to the parameter; std::swap
doesn't work on values. So whatever efficiency you think will be lost by using references will be lost either way.
The principle arguments in favor of copy/move&swap are:
Reducing code duplication. This is only advantageous if your implementation of copy/move assignment operations would be more or less identical to the copy/move construction. This is not true of many types; as previously stated, vector
can optimize itself quite a bit by using existing storage where possible. Indeed many containers can (particularly sequence containers).
Providing the strong exception guarantee with minimal effort. Assuming your move constructor is noexcept.
Personally, I prefer to avoid the scenario altogether. I prefer letting the compiler generate all of my special member functions. And if a type absolutely needs me to write those special member functions, then this type will be as minimal as possible. That is, it's sole purpose will be managing whatever it is that requires this operation.
That way, I just don't have to worry about it. The lion's share of my classes don't need any of these functions to be explicitly defined.
I realize that this has an accepted answer, but I feel I have to jump in. There are two different issues here:
If you are doing 1, you generally do 2. Because you need to implement the swap/move assignment logic somewhere, and you can't implement it in the unified assignment operator, so generally you implement swap and call it. But doing 2 does not mean you have to do 1:
T& operator=(T&&) { /* actually implemented */ }
T& operator=(const T& t) { T t2(t); swap(*this, t2); return *this;}
In this case, we implement move assignment, but use the default swap (which does a move construction and two move assignments).
The motivation for doing CAS is to get the strong exception guarantee, though as T.C. points out in the comments, you can do:
T& operator=(const T& t) { *this = T(t); return *this;}
which is potentially more efficient.In most of the code that I write, performance is an issue and I have never needed the strong exception guarantee, so I would almost never do this, so it just depends on your use case.
You should do 1 never. It's preferable that they be separate functions so that move assignment can be marked noexcept.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With