jmoxoby
jmoxoby

Reputation: 11

C++ optimization: avoid a copy operation

I have the following:

#include <iostream>
#include <utility>

class T {
public:
    T() {std::cout << "Default constructor called" << std::endl;}
    T(const T&) {std::cout << "Copy constructor called" << std::endl;}
};

static T s_t;

T foo() {
    return s_t;
}

class A {
public:
    A() = delete;
    A(T t) : _t(t) {}
private:
    T _t;
};

int main() {

    std::cout << "Starting main" << std::endl;
    A a(foo());
    return 0;

}

When I compile it with the line: g++ -std=c++17 -O3 test.cpp and run it, I get the following output

Default constructor called
Starting main
Copy constructor called
Copy constructor called

My question is: since the constructor of A is taking an r-value object of type T that is only used to initialize _t, is it possible for the compiler to avoid the second copy operation and just copy s_t directly into _t?

(I understand that I can potentially replace the second copy with a move by implementing a move constructor for T)

Upvotes: 1

Views: 266

Answers (1)

Max Langhof
Max Langhof

Reputation: 23691

Your copy constructor has an observable side effect (output). The (almost) golden rule of C++ optimizations is the as-if rule: Observable side effects must not be changed. But one of the two exceptions from this rule is the elision of some copy operations, specified here:

http://eel.is/c++draft/class.copy.elision

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects.

But this is only allowed in very specific circumstances:

This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object [...]

Your variable does not have automatic storage duration (i.e. it is not a local variable).

  • in a throw-expression, [...]

We are not a throw-expression.

  • in a coroutine [...]

We are not in a coroutine.

  • when the exception-declaration of an exception handler ([except]) declares an object of the same type (except for cv-qualification) as the exception object ([except.throw]) [...]

This is not the case either.

Neither the return of a global variable from a function, nor the use of a constructor parameter to initialize a member variable is covered here, so it would be illegal for any compiler to elide these copies in the code you show.

That said, if the compiler can prove that there are no side effects (which, in the case of most compilers, include system calls and memory allocations), it is of course free to elide the copy under the as-if rule, just like for all other optimizations.

Upvotes: 1

Related Questions