Unexpected copy constructor

Viewed 100

In the following example I am expecting only a single copy-construction, as I thought the intermediate copies would by copy elided. The only required (I thought?) copy would be in the constructor of B to initialize the member variable a.

#include <iostream>

struct A
{
    A() = default;
    A(A const&) { std::cout << "copying \n"; }
};

struct B
{
    B(A _a) : a(_a) {}
    A a;    
};

struct C : B
{
    C(A _a) : B(_a) {}
};

int main()
{
    A a{};
    C c(a);
}

When I execute this code (with -O3) I see the following output

copying 
copying 
copying 

Why aren't these intermediate copies elided?

3 Answers

Here are the cases, where copy elision is allowed (class.copy/31):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv- unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

  • in a throw-expression, when the operand is the name of a non-volatile automatic object (other than a function or catch-clause parameter) whose scope does not extend beyond the end of the innermost enclosing try-block (if there is one), the copy/move operation from the operand to the exception object (15.1) can be omitted by constructing the automatic object directly into the exception object

  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move
  • when the exception-declaration of an exception handler (Clause 15) declares an object of the same type (except for cv-qualification) as the exception object (15.1), the copy/move operation can be omitted

None of these are true for your example (we are not in a return statement, throw-expression or exception-declaration. And there are no temporaries in your example at all.), so copy happens each time you expect to happen.

Note, that copy elision is allowed in the mentioned cases, but not mandatory. So even, for these cases, the compiler is allowed to emit copies (this is true for C++11. In C++17, there are some cases, where copy elision is mandatory. But, none of your example cases allows elision in C++17 either.)

Expanding on the discussion under the other answer, the copy here has side effects (printing to console), and so does not qualify for copy optimization.

Copy elision, however, is allowed regardless of side effects, per the cppreference article on copy elision. It's not allowed here because you have lvalues all the way. None of the copies you hoped to eliminate were of an rvalue or prvalue. To make them an rvalue, you need to cast using std::move or construct them as nameless temporaries as part of the constructor call.

Again, the cppreference article explains it much better.

You are passing the a object to the C constructor by value. And then it is being passed to the B constructor by value.

Related