Sunday, 10 April 2016

c++11 - c++ constructor and copy constructor



I am trying to understand the following code's behavior



/* code block 1 */
#include
class A
{
private:

int value;

public:
A(int n) { std::cout << "int n " << std::endl; value = n; }
A(const A &other) { std::cout << " other " << std::endl; value = other.value; }
// A (A &&other) { std::cout << "other rvalue" << std::endl; value = other.value; }

void print(){ std::cout << "print " << value << std::endl; }

};


int main(int argc, char **argv)
{
A a = 10;
A b = a;
b.print();
return 0;
}



When I compile the code above, it works as I expect



/* code block 2 */
g++ -std=c++11 t.cpp
./a.out
int n
other
print 10



When I remove the const from copy constructor



/* code block 3 */
class A
{
...
A(int n) { std::cout << "int n " << std::endl; value = n; }
A(A &other) { std::cout << " other " << std::endl; value = other.value; }
// A (A &&other) { std::cout << "other rvalue" << std::endl; value = other.value; }
}



The compiler won't compile



/* code block 4 */
t.cpp:19:5: error: no viable constructor copying variable of type 'A'
A a = 10;
^ ~~
t.cpp:9:4: note: candidate constructor not viable: no known conversion from 'A' to 'int' for 1st argument
A(int n) { std::cout << "int n " << std::endl; value = n; }

^
t.cpp:10:4: note: candidate constructor not viable: expects an l-value for 1st argument
A(A &other) { std::cout << " other " << std::endl; value = other.value; }


from the result t.cpp:9:4, it seems the compiler try to convert A to int, but the code is A a = 10;, If I am the compiler, I will either




  1. trying to initialized a temporary variable with type A from integer 10, and then use copy constructor A(A &other) to initialize a


  2. initialize a with constructor function A(int) directly





I am confusing about the compiler's output from t.cpp:9:4



from the output t.cpp:10:4, compiler says it expect an l-value copy constructor, so I change to code to



/* code block 5 */
class A
{
...

A(int n) { std::cout << "int n " << std::endl; value = n; }
A(A &other) { std::cout << " other " << std::endl; value = other.value; }
A (A &&other) { std::cout << "other rvalue" << std::endl; value = other.value; }
}


When I follow the hint to define a rvalue copy constructor, the output shows that the rvalue copy constructor wasn't called



/* code block 6 */
g++ -std=c++11 t.cpp

int n
other
print 10


Questions:




  1. (in code block 3) why can't I remove the const from copy constructor?

  2. (in code block 4 -> t.cpp:9:4) why would the compiler try to convert from 'A' to 'int'?


  3. (in code block 5) the compiler says that it need a rvalue copy constructor(from code block 4 -> t.cpp:10:4), so I define one, but the running output show the rvalue copy constructor wasn't called, why?


Answer



What you are seeing is called copy elision in a pre-C++17 compiler (try it out with C++17 on compiler explorer or wandbox with -std=c++17 vs. -std=c++14 flags). As of C++17 the compiler is required to eliminate many cases of copy and move constructors, and construct objects directly without any intermediate objects.



Unlike



A a { 10 };



the line



A a = 10;


means that a temporary object is constructed first, as if the code has:



A a = A(10);



Until C++17, the compiler was allowed to optimize this code, and construct a directly from 10 without the temporary object. Note that the emphasis is that it was allowed but not required to do this copy elision optimization. You have observed this allowed optimization.



The compiler had to compile or fail the code regardless of its decision to do copy elision. If the compiler could not call the copy constructor, like in your case, then it had to fail the compilation unconditionally, even if it decided to do copy elision. This changed with C++17, and the compiler is now required to make the copy elision optimization in this case. Since it is guaranteed to elide the copy constructor, no copy constructor is even required and the code can compile without an error.



Note the copy constructor without const:



A(A &other) { std::cout << " other " << std::endl; value = other.value; }


Without copy elision, this copy constructor can't be used for:




A a = A(10);


It can't be used because A(10) is a temporary object, and as such can be passed as an rvalue parameter to constructors and methods like



A(A && other);
foo(A && other);



or passed as a const lvalue reference parameter to constructors and methods like



A(const A& other);
bar(const A& other);


But it can't be passed as a regular mutable parameter (like in your code block 3).



With copy elision it does not even try to call the copy or the move constructor in these cases.




It still needs to call the copy constructor for



A b = a;


and it can do that with a mutable parameter, only because a is neither a temporary nor a const object. If you make a const then the code will fail to compile, when the copy constructor does not get a const (for C++17 and earlier):



const A a = 10;
A b = a;
// ^^ this will fail



Fun note: The following line will is guaranteed not to call the copy constructor even once with C++17:



A a = A(A(A(A(1))));

No comments:

Post a Comment

c++ - Does curly brackets matter for empty constructor?

Those brackets declare an empty, inline constructor. In that case, with them, the constructor does exist, it merely does nothing more than t...