Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 989
Lingxi Avatar asked Jan 15 '16 04:01

Lingxi


People also ask

What is the difference between the move and copy operator why would you use one or the other?

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.

What is the difference between move constructor and move assignment?

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.

What is the difference between a copy and move operation C++?

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.


2 Answers

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.

like image 95
Nicol Bolas Avatar answered Nov 27 '22 11:11

Nicol Bolas


I realize that this has an accepted answer, but I feel I have to jump in. There are two different issues here:

  1. Unified assignment operator. This means that you have one assignment operator that takes by value, instead of two overloads that take by const & and &&.
  2. A copy and swap (CAS) copy assignment operator.

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.

like image 43
Nir Friedman Avatar answered Nov 27 '22 11:11

Nir Friedman