(I'm using gcc with -O2
.)
This seems like a straightforward opportunity to elide the copy constructor, since there are no side-effects to accessing the value of a field in a bar
's copy of a foo
; but the copy constructor is called, since I get the output meep meep!
.
#include <iostream>
struct foo {
foo(): a(5) { }
foo(const foo& f): a(f.a) { std::cout << "meep meep!\n"; }
int a;
};
struct bar {
foo F() const { return f; }
foo f;
};
int main()
{
bar b;
int a = b.F().a;
return 0;
}
It is neither of the two legal cases of copy ctor elision described in 12.8/15:
Return value optimisation (where an automatic variable is returned from a function, and the copying of that automatic to the return value is elided by constructing the automatic directly in the return value) - nope. f
is not an automatic variable.
Temporary initializer (where a temporary is copied to an object, and instead of constructing the temporary and copying it, the temporary value is constructed directly into the destination) - nope f
is not a temporary either. b.F()
is a temporary, but it isn't copied anywhere, it just has a data member accessed, so by the time you get out of F()
there's nothing to elide.
Since neither of the legal cases of copy ctor elision apples, and the copying of f
to the return value of F()
affects the observable behaviour of the program, the standard forbids it to be elided. If you got replaced the printing with some non-observable activity, and examined the assembly, you might see that this copy constructor has been optimised away. But that would be under the "as-if" rule, not under the copy constructor elision rule.
Copy elision happens only when a copy isn't really necessary. In particular, it's when there's one object (call it A) that exists for the duration of the execution of a function, and a second object (call it B) that will be copy constructed from the first object, and immediately after that, A will be destroyed (i.e. upon exit from the function).
In this very specific case, the standard gives permission for the compiler to coalesce A and B into two separate ways of referring to the same object. Instead of requiring that A be created, then B be copy constructed from A, and then A be destroyed, it allows A and B to be considered two ways of referring to the same object, so the (one) object is created as A, and after the function returns starts to be referred to as B, but even if the copy constructor has side effects, the copy that creates B from A can still be skipped over. Also, note that in this case A (as an object separate from B) is never destroyed either -- e.g., if your dtor also had side effects, they could (would) be omitted as well.
Your code doesn't fit that pattern -- the first object does not cease to exist immediately after being used to initialize the second object. After F()
returns, there are two instances of the object. That being the case, the [Named] Return Value Optimization (aka. copy elision) simply does not apply.
Demo code when copy elision would apply:
#include <iostream>
struct foo {
foo(): a(5) { }
foo(const foo& f): a(f.a) { std::cout << "meep meep!\n"; }
int a;
};
int F() {
// RVO
std::cout << "F\n";
return foo();
}
int G() {
// NRVO
std::cout << "G\n";
foo x;
return x;
}
int main() {
foo a = F();
foo b = G();
return 0;
}
Both MS VC++ and g++ optimize away both copy ctors from this code with optimization turned on. g++ optimizes both away even if optimization is turned off. With optimization turned off, VC++ optimizes away the anonymous return, but uses the copy ctor for the named return.
The copy constructor is called because a) there is no guarantee you are copying the field value without modification, and b) because your copy constructor has a side effect (prints a message).
A better way to think about copy elision is in terms of the temporary object. That is how the standard describes it. A temporary is allowed to be "folded" into a permanent object if it is copied into the permanent object immediately before its destruction.
Here you construct a temporary object in the function return. It doesn't really participate in anything, so you want it to be skipped. But what if you had done
b.F().a = 5;
if the copy were elided, and you operated on the original object, you would have modified b
through a non-reference.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With