Search code examples
c++c++11moveassignment-operatorlibrary-design

Implementing copy and move assignment with a single function


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.


Solution

  • 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:

    1. 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).

    2. 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.