C++ copy constructor activation in return-by-vlaue

274 Views Asked by At

I wasn't able to find a concrete answer for the following question:

Consider the following code:

Obj f() {
    Obj o2;
    return o2;
}

int main() {
    Obj o1 = f();
    return 0;
}

How many times is the copy constructor activated without compiler optimization?

In case there isn't move constructor, isn't it one time for copying o2 to the caller function and another time for constructing o1?

In case there is move constructor, isn't it one time for copying o2 to the caller function and another time for constructing o1 (2nd time is move const)?

2

There are 2 best solutions below

0
On BEST ANSWER

Before C++17, yes there are two copy constructor calls (both of which can be elided even if they have side-effects). You can see this with -fno-elide-constructors in gcc/clang.

Since C++17 due to the new temporary materialization rules there is just one copy involved inside f (which, again, can be elided).


To be accurate all of them are moves, not copies.

0
On

C++03 and before

Obj is copied twice. Once by the return statement (constructing the return value), and once to initialize o1 by copying the return value.

C++11 and C++14

If Obj has a usable move constructor, it is moved twice and copied zero times. The return statement must use a move, even though the returned expression is an lvalue. This "move optimization" is mandatory due to a special rule in the language; o2 must not be copied, even when optimizations are disabled. A second move occurs when o1 is initialized.

If Obj has either no move constructor or an implicitly deleted move constructor, the copy constructor is used twice.

If Obj has an explicitly deleted move constructor, the program is ill-formed since the initialization of o1 tries to use the deleted move constructor.

C++17 and later

If Obj has a usable move constructor, it is moved once, when the return statement is executed. As mentioned above, it is mandatory for the compiler to use a move instead of a copy. The construction of o1 involves neither a copy nor a move. Rather, the return statement in f() initializes o1, without the involvement of a temporary. This is because of "guaranteed copy elision": the language requires the copy to be elided, even if optimizations are disabled. This is because f() is a prvalue, and a prvalue is not materialized (i.e., instantiated as a temporary object) unless it is necessary to do so. The "legal fiction" created by the standard is that f() actually returns a "recipe" for creating Obj, not an Obj itself. In practice, this can be implemented the same way that the (optional) return value optimization was implemented in earlier versions of the standard: the caller passes a pointer to o1 directly into f, and the return statement constructs Obj into this pointer.

If the move constructor of Obj is implicitly deleted or does not exist, the copy constructor will be used by the return statement, so there will be one copy and zero moves.

If the move constructor of Obj is explicitly deleted, the program is ill-formed as in the C++11/C++14 case.

In all cases

The copies/moves in the above situations can be optimized out. In cases that involve more than one copy/move operation, the compiler can optimize out any or all of them.