zpoint
zpoint

Reputation: 121

c++ constructor and copy constructor

I am trying to understand the following code's behavior

/* code block 1 */
#include <iostream>
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?

Upvotes: 3

Views: 1376

Answers (3)

Samridhi Grover
Samridhi Grover

Reputation: 23

When your code runs

A a = 10;

It cast's 10 to a variable of type A and calls the copy constructor to initialize 'a'. Because 10 does not have a reference in memory, it is an rvalue. And as your copy constructor takes in 'other' by reference you cannot pass in an rvalue as there is no reference to be modified if it is modified. C++ allows rvalues to be passed in by reference only if it is by const reference.

The reason it's trying to cast it to an int is that you only have two constructors: one that takes in an int and another that takes in an object of type 'A' by reference. Since it already cast 10 to an object of type 'A', and that object is an rvalue, it's simply stating that neither constructor can take in that object.

As a note: this is a reminder to always pass by const reference when the object is not being modified.

Upvotes: 1

bornfree
bornfree

Reputation: 2538

When you write

A a = 10;

Compiler converts 10 to a temporary object and then calls copy constructor to create a.

A a = A(10);

Consider this program,

#include <iostream>
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 lvalue" << std::endl; value = other.value; }

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

};

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

and compile it with

g++ t.cpp -std=c++11

On running the program, its output is

int n 

Now you may wonder why the copy constructor A(const A &other) is not called. This is because of copy elision in C++. A compiler can optimize the call to copy constructor and directly call the matching constructor. So instead of A a = A(10); what gets called is this A a(10);

If you want to disable copy elision, compile the above program with

g++ t.cpp -std=c++11 -fno-elide-constructors

Now on running the program you can see below output

int n 
 other 

There is no copy elision. So, A a = A(10); gets called. First a temporary object is created and then copy constructor is called to create a.

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

Because temporary objects can't be bound to lvalue reference. They can be bound only to rvalue reference or const lvalue reference. A(10) creates a temporary object which can only be bound to either const lvalue reference(const A&) or to rvalue reference (A&&).

(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?

This happens because of copy elision. Compile it with -fno-elide-constructors and then you can see the call to the rvalue constructor. See below.

#include <iostream>
class A
{
        private:
            int value;

        public:
            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 lvalue" << std::endl; value = other.value; }

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

};

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

Compile:

g++ t.cpp -std=c++11 -fno-elide-constructors

output

int n 
other lvalue

Upvotes: 1

Michael Veksler
Michael Veksler

Reputation: 8475

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))));

Upvotes: 5

Related Questions