kerk_pilos
kerk_pilos

Reputation: 63

Understanding the reasoning between copy/move constructors and operators

I am trying to get the grasp of rvalue references and move semantics with a simple self-made example but I can't understand a specific part. I have created the following class:

class A {
public:
    A(int a) {
        cout << "Def constructor" << endl;
    }

    A(const A& var) {
        cout << "Copy constructor" << endl;
    }

    A(A&& var) {
        cout << "Move constructor" << endl;
    }

    A& operator=(const A& var) {
        cout << "Copy Assignment" << endl;
        return *this;
    }

    A& operator=(A&& var) {
        cout << "Move Assignment" << endl;
        return *this;
    }
};

I tried the following experiments to see if I can predict how the constructors/operators are going to be called:

  1. A a1(1) - The default constructor is going to be called. PREDICTED.
  2. A a2 = a1 - The copy constructor is going to be called. PREDICTED.
  3. a1 = a2 - The copy assignment operator is going to be called. PREDICTED.

Now, I created a simple function that just returns an A object.

A helper() {
   return A(1);
}
  1. A a3 = helper() - The default constructor is going to be called in order to create the object that the helper returns. The move constructor is not going to be called due to RVO. PREDICTED.
  2. a3 = helper() - The default constructor is going to be called in order to create the object that the helper returns. Then, the move assignment operator is going to be called. PREDICTED.

Now comes the part I don't understand. I created another function that is completely pointless. It takes an A object by value and it just returns it.

A helper_alt(A a) {
    return a;
}
  1. A a4 = helper_alt(a1) - This will call the copy constructor, to actually copy the object a1 in the function and then the move constructor. PREDICTED.
  2. a4 = helper_alt(a1) - This will call the copy constructor, to actually copy the object a1 in the function and then I thought that the move assignment operator is going to be called BUT as I saw, first, the move constructor is called and then the move assignment operator is called. HAVE NO IDEA.

Please, if any of what I said is wrong or you feel I might have not understood something, feel free to correct me.

My actual question: In the last case, why is the move constructor being called and then the move assignment operator, instead of just the move assignment operator?

Upvotes: 6

Views: 1413

Answers (2)

Mahdi
Mahdi

Reputation: 171

consider the below example. I have compiled the sample code using -fno-elide-constructors flag to prevent RVO optimizations:
g++ -fno-elide-constructors -o test test.cpp

#include<iostream>

using namespace std;

class A {
public:
    A(int a) {
        cout << "Def constructor" << endl;
    }

    A(const A& var) {
        cout << "Copy constructor" << endl;
    }

    A(A&& var) {
        cout << "Move constructor" << endl;
    }

    A& operator=(const A& var) {
        cout << "Copy Assignment" << endl;
        return *this;
    }

    A& operator=(A&& var) {
        cout << "Move Assignment" << endl;
        return *this;
    }
};

A a_global(1);

A helper_alt(A a) {
    return a;
}

A helper_a_local(A a) {
    A x(1);
    return x;
}

A helper_a_global(A a) {
    return a_global;
}

int main(){
    A a1(1);
    A a4(4);
    std::cout << "================= helper_alt(a1) ==================" << std::endl;
    a4 = helper_alt(a1);
    std::cout << "=============== helper_a_local()   ================" << std::endl;
    a4 = helper_a_local(a1);
    std::cout << "=============== helper_a_global()  ================" << std::endl;
    a4 = helper_a_global(a1);
    return 0;
}

This will result in the below output:

Def constructor
Def constructor
Def constructor
================= helper_alt(a1) ==================
Copy constructor
Move constructor
Move Assignment
=============== helper_a_local()   ================
Copy constructor
Def constructor
Move constructor
Move Assignment
=============== helper_a_global()  ================
Copy constructor
Copy constructor
Move Assignment

In simple words, C++ constructs a new temporary object (rvalue) when the return type is not a reference, which results in calling Move or Copy constructor depending on the value category and the lifetime of the returned object.
Anyway, I think the logic behind calling the constructor is that you are not working with reference, and returned identity should be construed first, either by copy or move constructor, depending on the returned value category or lifetime of the return object. As another example:

A helper_move_vs_copy(A a) {
    // Call the Copy Constructor
    A b = a;
    // Call the Move Constructor, Due to the end of 'a' lifetime
    return a;
}

int main(){
    A a1(1);
    A a2(4);
    std::cout << "=============== helper_move_vs_copy()  ================" << std::endl;
    helper_move_vs_copy(a1);
    return 0;
}

which outputs:

Def constructor
Def constructor
=============== helper_move_vs_copy()  ================
Copy constructor
Copy constructor
Move constructor

From cppreference:

an xvalue (an “eXpiring” value) is a glvalue that denotes an object whose resources can be reused;

At last, it is the job of RVO to decrease unnecessary moves and copies by optimization of the code, which can even result in an optimized binary for basic programmers!

Upvotes: 2

Klaus
Klaus

Reputation: 25593

Congratulations, you found a core issue of C++!

There are still a lot of discussions around the behavior you see with your example code.

There are suggestions like:

A&& helper_alt(A a) {
    std::cout << ".." << std::endl;
    return std::move(a);
}

This will do what you want, simply use the move assignment but emits a warning from g++ "warning: reference to local variable 'a' returned", even if the variable goes immediately out of scope.

Already other people found that problem and this is already made a c++ standard language core issue

Interestingly the issue was already found in 2010 but not solved until now...

To give you an answer to your question "In the last case, why is the move constructor being called and then the move assignment operator, instead of just the move assignment operator?" is, that also C++ committee does not have an answer until now. To be precise, there is a proposed solution and this one is accepted but until now not part of the language.

From: Comment Status

Amend paragraph 34 to explicitly exclude function parameters from copy elision. Amend paragraph 35 to include function parameters as eligible for move-construction.

Upvotes: 3

Related Questions