C++ move-assignment prevents copy-swap idiom
Asked Answered
B

2

5

In C++, copy-swap idiom is typically implemented like this:

C& operator=(C rhs)
{
    swap(*this, rhs);
    return *this;
}

Now, if I want to add a move-assignment operator, it is supposed to look like this:

C& operator=(C&& rhs)
{
    swap(*this, rhs);
    return *this;
}

However, this creates ambiguity about which assignment operator should be called and compilers rightfully complain about it. So my question is the following: If I want to support copy-swap idiom together with move-assignment semantics what am I supposed to do?

Or is this a non-issue as having a move-copy constructor and copy-swap idiom, one doesn't really benefit from having a move-assignment operator?

After asking this question, I've written a code that demonstrates that move-assignment may result in fewer function calls than copy-swap idiom. First let me present my copy-swap version. Please bear with me; it appears like a long but a simple example:

#include <algorithm>
#include <iostream>
#include <new>

using namespace std;

bool printOutput = false;

void* operator new(std::size_t sz)
{
    if (printOutput)
    {
        cout << "sz = " << sz << endl;
    }

    return std::malloc(sz);
}

class C
{
    int* data;

    public:

    C() : data(nullptr)
    {
        if (printOutput)
        {
            cout << "C() called" << endl;
        }
    }

    C(int data) : data(new int)
    {
        if (printOutput)
        {
            cout << "C(data) called" << endl;
        }

        *(this->data) = data;
    }

    C(const C& rhs) : data(new int)
    {
        if (printOutput)
        {
            cout << "C(&rhs) called" << endl;
        }

        *data = *(rhs.data);
    }

    C(C&& rhs) : C()
    {
        if (printOutput)
        {
            cout << "C(&&rhs) called" << endl;
        }

        swap(*this, rhs);
    }

    C& operator=(C rhs)
    {
        if (printOutput)
        {
            cout << "operator= called" << endl;
        }

        swap(*this, rhs);

        return *this;
    }

    C operator+(const C& rhs)
    {
        C result(*data + *(rhs.data));

        return result;
    }

    friend void swap(C& lhs, C& rhs);

    ~C()
    {
        delete data;
    }
};

void swap(C& lhs, C& rhs)
{
    std::swap(lhs.data, rhs.data);
}

int main()
{
    C c1(7);
    C c2;

    printOutput = true;

    c2 = c1 + c1;

    return 0;
}

I have compiled this with g++ using the -fno-elide-constructors option as I want to see the no optimization behavior. The result is the following:

sz = 4
C(data) called   // (due to the declaration of result)
C() called       // (called from the rvalue copy-constructor)
C(&&rhs) called  // (called due to copy to return temporary)
C() called       // (called from the rvalue copy-constructor)
C(&&rhs) called  // (called due to pass-by-value in the assignment operator)
operator= called

Now, if I choose not to make copy-swap idiom in the assignment operator, I will have something like this:

C& operator=(const C& rhs)
{
    if (printOutput)
    {
        cout << "operator=(const C&) called" << endl;
    }

    if (this != &rhs)
    {
        delete data;

        data = new int;
        *data = *(rhs.data);
    }

    return *this;
}

This allows me to have the move-assignment operator as follows:

C& operator=(C&& rhs)
{
    if (printOutput)
    {
        cout << "operator=(C&&) called" << endl;
    }

    swap(*this, rhs);

    return *this;
}

Now, with everything else being the same, I get the following output:

sz = 4
C(data) called        // (due to the declaration of result)
C() called            // (called from the rvalue copy-constructor)
C(&&rhs) called       // (called due to copy to return temporary)
operator=(C&&) called // (move-assignment)

As you can see this results in fewer function calls. Actually the last three function calls in the copySwapIdiom has now dropped down to a single function call. This is expected as we no longer pass the assignment operator parameter by value, hence no construction happens there.

However, I do not benefit from the beauty of copy-swap idiom in the assignment operator. Any insight is much appreciated.

Bunko answered 12/4, 2018 at 12:13 Comment(4)
"is typically implemented like this" - To avoid writing two overloads, one for moving rvalues and one for copying lvlaues...Mot
Short answer: use move semantics. That's what it's there for.Cuckoo
C& operator=(C rhs) IS a move-assignment operator. Objects passed by value can be initialized from rvalues. You don't need to add anything elseColic
The best answer that I received was a comment from M.M. to my incorrectly created answer. I am copy/pasting it here so it doesn't get lost: "This should be part of the question. Although I will save you some time and say: yes, the copy-swap idiom involves more constructor calls than having separate copy-assignment and move-assignment operators. You trade off performance against code simplicity. This is discussed in more depth on stackoverflow.com/questions/3279543 – M.M"Bunko
B
9

There actually isn't a need to implement the move assignment operator if you provide a valid move constructor.

class Foo
{
public:
   explicit Foo(Bar bar)
       : bar(bar)
    { }

    Foo(const Foo& other)
        : bar(other.bar)
    { }

    Foo(Foo&& other)
        : bar(other.bar)
    { }

    // other will be initialized using the move constructor if the actual
    // argument in the assignment statement is an rvalue
    Foo& operator=(Foo other)
    {
        std::swap(bar, other.bar);
        return *this;
    }
Biyearly answered 12/4, 2018 at 12:29 Comment(2)
In fact, this is one of the main reasons for using copy and swap: You contain all your complicated behavior in as few functions as possible instead of having to replicate it into several.Monte
Did you mean to use std::swap or swap?Singlehanded
C
7

The motivation behind the copy-swap idiom here is to forward the copy/move work to constructors, so that you don't duplicate work for both constructors and assignment operators. That said,

C& operator=(C rhs) noexcept;

means to replace the pair

C& operator=(const C& rhs);
C& operator=(C&& rhs) noexcept;

Whether C& operator=(C rhs) noexcept; performs copy or move assignment depends on how rhs is constructed. For example,

a = std::move(b); // rhs is move-constructed from r-value std::move(b), and thus move-assignment
c = d;            // rhs is copy-constructed from l-value d, and thus copy-assignment
Confucianism answered 12/4, 2018 at 13:57 Comment(2)
I did not get this. How the program knows how the object was constructed?Glans
@EduardRostomyan Updated answer to clarify.Confucianism

© 2022 - 2024 — McMap. All rights reserved.