views:

160

answers:

4

(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;
}
+1  A: 

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

Michael Petito
Copy constructors with side effects can be elided. Bottom line - don't write copy constructors like that.
anon
+8  A: 

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.

Steve Jessop
Interesting, thanks. Why doesn't the standard allow this case, though?
Jesse Beder
Steve Jessop
So I suspect the reason no case of elision was added to cover your example, is that you can do it yourself, and in the absence of need, behaviour-breaking special cases are bad. That's just a guess, though.
Steve Jessop
@Steve, that sounds about right; although I always viewed copy elision not in terms of utility, but in terms of theory (in the prove-your-program sense). I (obviously) hadn't read the standard, so I imagined it said something like, "the compiler may assume a copy constructor makes an ideal copy". I'm still not sure why they don't do something like that, but maybe it's along the lines of what you said: no one needs it.
Jesse Beder
I guess it was considered a step too far for `foo a; foo b(a);` to not call the copy constructor (when the call is observable). I mean, that code could hardly be more clear that it wants it called. If neither `a` nor `b` is modified later in the function, then all else being equal the compiler can just elide `b`, and turn all references to it into references to `a`. But if all else isn't equal (i.e. if the copy is observable), the standard says that the user has demanded a copy, and so the user gets a copy.
Steve Jessop
A: 

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.

Potatoswatter
But the compiler might only elide the copy if it's not used as an lvalue.
Jesse Beder
@Jesse: in my example it's not used as an lvalue. It's used as the left-hand side of the `.` operator.
Potatoswatter
@Potatoswatter - but then the result of the `.` operator is used as an lvalue. I'm not 100% certain about my C++ terminology, so maybe lvalue isn't the right word, but the concept I'm looking for should be transitive.
Jesse Beder
@Jesse: The problem is, *anything* can return a non-const reference the the inside of the object. For example, `F().get_a() = 5` or `get_a( F() ) = 5`. Copy elision is a special case for a common pattern, which you are not following. To the casual reader, it looks like you created a side effect and intend to see it happen.
Potatoswatter
@Potatoswatter - so in those cases, the copy wouldn't be elided. I'm suggesting that a compiler *could* determine if it actually needs a copy, and if so, perform the copy. If `foo::get_a()` returns a reference to a field inside `foo`, then it *wouldn't* be elided. But if `foo::get_a()` returns a copy of a field, then `foo` doesn't need to be copied. The point is, in *this case* (and others), there is no observable difference between making a copy and not, *assuming the copy constructor makes an "ideal" copy* (which it doesn't here; but this is the same assumption as in usual copy elision).
Jesse Beder
@Jesse: You're essentially invoking the "as-if" rule. In fact, the compiler doesn't have to make a copy if it has utterly no effect. In your case, you introduced a side-effect which has nothing to do with copying, so the compiler invoked the side effect. For that matter, maybe it *didn't* make a copy within your program. Maybe it just printed something as a token gesture.
Potatoswatter
+2  A: 

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.

Jerry Coffin