6

(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;
}
Jesse Beder
  • 33,081
  • 21
  • 109
  • 146
  • Are you asking why the copy constructor is called instead of simply returning the field value of the original instance? – Michael Petito Apr 26 '10 at 22:31
  • 1
    How do you know it wasn't elided. And why do you think it should have been? Please modify your question with respect to these two queries. –  Apr 26 '10 at 22:33
  • @Neil, sorry, I thought it was clear, but it's edited. – Jesse Beder Apr 26 '10 at 22:36
  • My guess would be that it's because, while accessing the field itself doesn't have any side effects, the copy constructor does. Thus, accessing the field itself has the side effect of not having the side effects of the copy constructor. – Nick Lewis Apr 26 '10 at 22:38
  • I take that back, apparently side effects don't preclude elision. Very interesting... – Nick Lewis Apr 26 '10 at 22:40
  • Outputting "meep meep" is a fairly obvious side-effect. But the compiler could in fact ignore that, but may well not do so. Instead of depending on looking at program output, when it comes to optimisation, you need to look at the code emitted by the compiler. –  Apr 26 '10 at 22:43
  • @Neil, you're obviously right about the side-effect, but I was hoping the compiler would pretend there's an "ideal" copy constructor (as it does in the two cases Steve lists below) instead of actually analyzing the one implemented. – Jesse Beder Apr 26 '10 at 22:47

4 Answers4

11

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
  • 273,490
  • 39
  • 460
  • 699
  • 1
    Interesting, thanks. Why doesn't the standard allow this case, though? – Jesse Beder Apr 26 '10 at 22:50
  • For the same reason that it doesn't allow just any old function to be omitted, that contains a call to `std::cout<<`. The big question is, why does the standard *ever* allow "optimisations" that change the observable behaviour of the program? The answer being, that it was deemed needed to avoid ridiculous chains of copying of temporaries and return values, and that nobody would want to rely on copying being performed in those two cases. If you want to avoid copying in your case you can return a `const foo &`. In the two legal cases of copy ctor elision, you can't avoid a copy that way. – Steve Jessop Apr 26 '10 at 23:00
  • 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 Apr 26 '10 at 23:01
  • 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 Apr 26 '10 at 23:10
  • 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 Apr 26 '10 at 23:20
2

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
  • 476,176
  • 80
  • 629
  • 1,111
1

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
  • 12,891
  • 4
  • 40
  • 54
  • 3
    Copy constructors with side effects can be elided. Bottom line - don't write copy constructors like that. –  Apr 26 '10 at 22:47
1

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
  • 134,909
  • 25
  • 265
  • 421
  • But the compiler might only elide the copy if it's not used as an lvalue. – Jesse Beder Apr 26 '10 at 22:50
  • @Jesse: in my example it's not used as an lvalue. It's used as the left-hand side of the `.` operator. – Potatoswatter Apr 26 '10 at 23:04
  • @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 Apr 26 '10 at 23:12
  • @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 Apr 26 '10 at 23:16
  • @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 Apr 26 '10 at 23:26
  • 2
    @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 Apr 26 '10 at 23:35