3

For a class A, can we use

A& operator=( A other ) { /* ... */ }

instead of

A& operator=( const A&  other ) { /* ... */ }
A& operator=( const A&& other ) { /* ... */ }

without worsening performance or other negative effects?

Danh
  • 5,916
  • 7
  • 30
  • 45
abraham_hilbert
  • 2,221
  • 1
  • 13
  • 30

3 Answers3

6

Actually, you can implement copy-and-swap with

A& operator=( A other ) { swap(other, *this); return *this; }

for both copy and move assignment but the unconditional copy for self-assignment operator will downgrade performance.

Except, above function can't be marked noexcept, move should be lightweight, cheap, it isn't required but it should be lightweight. Hence, move constructor/assignment should be noexcept. without noexcept move constructor/assignment operator, move_if_noexcept can't be called when the container grows, the container will go back to copy.

Hence, the recommended way is:

A& operator=( const A&  other ) {
    if (std::addressof(other) != this) {
         A(other).swap(*this); // Copy constructor may throw
    }
    return *this;
}
A& operator=( A&& other ) noexcept { // note no const here
    // we don't check self assignment because
    // an object binds to an rvalue reference it is one of two things:
    //    - A temporary.
    //    - An object the caller wants you to believe is a temporary.
    swap(other); 
    return *this;
}
Danh
  • 5,916
  • 7
  • 30
  • 45
  • 4
    In many cases a copy assignment operator like this will be sub-optimal because you are making an unconditional copy of `other` when `this` may already have capacity that can be reused. – Chris Drew Dec 01 '16 at 12:59
  • 2
    This answer is misleading. Firstly, this implementation does not prevent us from declaring the copy-assignment operator as `noexcept`. The only thing that can throw here is the parameter's copy constructor. But if that happens, it always happens in the caller's context (!). Which means that if `swap` is `noexcept` (as it should be), you can freely declare this "pass-by-value" assignment as `noexcept` as well. – AnT stands with Russia Dec 01 '16 at 17:26
  • 3
    Secondly, the whole idea of "pass-by-value" idiom is to relegate copying/moving duties to the class's constructors. In order to work properly and without loss of performance, the class has to properly and efficiently define both copy and move constructors. The latter can obviously be `noexcept`, which means that in moving contexts the above "pass-by-value" implementation of assignment operator will be completely exception-free. – AnT stands with Russia Dec 01 '16 at 17:26
  • @Chris Drew: Very good point. This is indeed a good example of a situation where "pass-by-value" idiom does not work well. – AnT stands with Russia Dec 01 '16 at 17:30
  • Omitting self-assignment check in the move-assignment operator is controversial, [see here](http://stackoverflow.com/a/9322542/1505939) – M.M Dec 02 '16 at 03:46
  • @M.M Correct me if I were wrong, the implementation of `std::vector` in libstdc++ also omit self-assignment check in the move-assignment. I thought it's reasonable to omit it. – Danh Dec 02 '16 at 03:55
  • And libc++ also omits that check – Danh Dec 02 '16 at 04:01
5

without worsening performance or other negative effects?

It depends on the data members and base classes of A, and can even depend on the bases and members of A's bases and members.

Here is a link to a part of a video that demonstrates that if A has a vector as a data member, then the copy/swap idiom can cost a 70% performance hit for the copy assignment operator on average. This is compared to the much easier approach of defaulting the assignment special members.

http://www.youtube.com/watch?v=vLinb2fgkHk&t=35m30s

class A
{
    std::vector<int> v_;
public:
    // ...
};

You can expect string data members to have the same impact as vector data members.

If you aren't sure for your A, and if performance is important to you, try it both ways and measure. This is precisely what is demonstrated in the video.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
4
  • Performance of "move" contexts might suffer slightly.

    If your class A is movable, then in moving contexts the pass-by-value copy-and-swap implementation will implement move as a sequence of two operations:

    1. Move the right-hand side (RHS) value to the parameter other by using A's move constructor.
    2. Move the data from other to *this (or swap other with *this).

    The second implementation (with dedicated move-assignment) can do the same thing in just one operation: it will move (or swap) the right-hand side value with *this directly.

    So, potentially, the copy-and-swap variant involves the overhead of one extra move.

  • Performance of "copy" contexts can suffer from slightly to severely.

    If your left-hand side (LHS) has some copyable internal resource (like a memory block) that can accommodate the corresponding RHS value without reallocation, then a dedicated implementation of copy-assignment just needs to copy the data from RHS resource to LHS resource. No memory allocation is necessary.

    In copy-and-swap variant the copy is created unconditionally and the aforementioned internal resource has to be allocated and deallocated unconditionally. This can have a major negative performance impact.

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765