12

I don't seem to get why would you use the move assignment operator:

CLASSA & operator=(CLASSA && other); //move assignment operator

over, the copy assignment operator:

CLASSA & operator=(CLASSA  other); //copy assignment operator

The move assignment operator takes an r-value reference only e.g.

CLASSA a1, a2, a3;
a1 = a2 + a3;

In the copy assignment operator, other can be constructor using a copy constructor or a move constructor (if other is initialized with an rvalue, it could be move-constructed --if move-constructor defined--).

If it is copy-constructed, we will be doing 1 copy and that copy can't be avoided.

If it is move-constructed then the performance/behavior is identical to the one produced by the first overload.

My questions are:

1- Why would one want to implement the move assignment operator.

2- If other is constructed from an r-value then which assignment operator would the compiler choose to call? And why?

Kam
  • 5,878
  • 10
  • 53
  • 97
  • 1) less work, 2) it'd be ambiguous. – Kerrek SB Nov 06 '14 at 05:20
  • So you are saying for 2) the first overload will be called? – Kam Nov 06 '14 at 05:24
  • `CLASSA & operator=(CLASSA && other);` is a [move assignment operator](http://en.cppreference.com/w/cpp/language/move_operator). Not sure how that changes what you are asking about. A copy assignment operator takes either a `CLASSA` or a `const CLASSA&`. – Radiodef Nov 06 '14 at 05:25
  • @Kam: No. An ambiguous overload means that overload resolution fails when you try to use the overload, which in turn makes the program ill formed. – Kerrek SB Nov 06 '14 at 05:27
  • The performance/behaviour will not be identical. There will be probably be one less move if you pass by r-value reference. Whether that matters will depend on how expensive it is to move `CLASSA`. – Chris Drew Nov 06 '14 at 05:43
  • Herb Sutter went into quite a bit of detail on this in his ["Back to the Basics! Essentials of Modern C++ Style" talk at CppCon](http://www.youtube.com/watch?v=xnqTKD8uD64&t=51m5s) – Chris Drew Nov 06 '14 at 05:55

2 Answers2

10

You are not comparing like-with-like

If you are writing a move-only type like std::unique_ptr then a move assignment operator would be your only choice.

The more typical case is where you have a copyable type in which case I think you have three options.

  1. T& operator=(T const&)
  2. T& operator=(T const&) and T& operator=(T&&)
  3. T& operator=(T) and move

Note that having both the overloads you suggested in one class is not an option as it would be ambiguous.

Option 1 is the traditional C++98 option and will perform fine in most cases. However, if you need to optimize for r-values you could consider Option 2 and add a move assignment operator.

It is tempting to consider Option 3 and pass-by-value and then move which I think is what you are suggesting. In that case you only have to write one assignment operator. It accepts l-values and at the cost of only one extra move accepts r-values and many people will advocate this approach.

However, Herb Sutter pointed out in his "Back to the Basics! Essentials of Modern C++ Style" talk at CppCon 2014 that this option is problematic and can be much slower. In the case of l-values it will perform an unconditional copy and will not reuse any existing capacity. He provides numbers to backup his claims. The only exception is constructors where there is no existing capacity to reuse and you often have many parameters so pass by-value can reduce the number of overloads needed.

So I would suggest you start with Option 1 and move to Option 2 if you need to optimize for r-values.

Chris Drew
  • 14,926
  • 3
  • 34
  • 54
2

Clearly, the two overloads are not equivalent:

  1. The assignment operator taking an rvalue reference only works with rvalues are on the right-hand side of the expression. To also support lvalues, another overload, e.g., using T const& would be needed for copyable types. Of course, for move-only types, like std::unique_ptr<T>, defining this assignment operator is the appropriate choice.
  2. The assignment operator taking a value covers both rvalue and lvalue assignments assuming the type in question is both copy- and move-constructible. Its canonical implementation is to call swap() to replace the object's state with the state from the right-hand side. It has the advantage that the copy/move construction of the argument can often be elided.

In any case, you wouldn't want to have both overloads in one class! When assigning from an lvalue, obviously, the version taking a value would be chosen (the other option isn't viable). However, both assignment operators are viable when assigning an rvalue, i.e., there would be an ambiguity. This can easily be verified by trying to compile this code:

struct foo
{
    void operator=(foo&&) {}
    void operator=(foo) {}
};

int main()
{
    foo f;
    f = foo();
}

To deal with a move- and copy construction separately you could define a pair of assignment operators using T&& and T const& as arguments. However, this results in having to implement two versions of essentially the same copy assignment while having just a T as argument requires just one copy assignment to be implemented.

Thus, there are two obvious choices:

  1. For a move-only type you'd define T::operator= (T&&).
  2. For a copyable type you'd define T::operator=(T).
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • 2
    [Herb sutter points out](https://www.youtube.com/watch?v=xnqTKD8uD64&t=1h03m44s) that using pass by-value in this way can be much slower than pass by reference-to-const due to making an unconditional copy and not reusing existing capacity. – Chris Drew Nov 06 '14 at 07:20