20

I was shown the following example in chat:

#include <iostream>
struct foo { ~foo() { std::cout << "destroying!\n"; } };
const foo& func(const foo& a, const foo&) { return a; }

int main()
{
  foo x;
  const foo& y = func(foo(), x);
  std::cout << "main\n";
}

Output:

destroying!
main
destroying!

It appears to demonstrate that the lifetime of the foo temporary is not extended to entirety of main, even though it gets bound to a ref-to-const in that scope.

Presumably, then, the lifetime extension only "works once"; that is, it is applied when func's arguments are initialised, but isn't passed on through consecutive bindings.

Is my interpretation correct? If so (and if any individual paragraph is directly applicable) what's the standard wording that defines this behaviour?

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055

4 Answers4

9

You're almost right. This behaviour actually comes from the function call specifically, not because of any sort of "only works once" rule.

Here's the wording for the whole lifetime extension "feature", with the pertinent rule emphasised in bold:

[C++11: 12.2/5]: [..] The temporary to which the reference is bound or the temporary that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference except:

  • [..]
  • A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.
  • [..]
Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • But in this case, the temporary is also bound to the const reference `y`. So which reference controls its lifetime? – James Kanze Feb 06 '13 at 17:58
  • 1
    @JamesKanze: I see the "except" list as taking precedence. – Lightness Races in Orbit Feb 06 '13 at 17:59
  • 1
    I sort of see what you mean, but I still think it's ambiguous, and if that's the intent, a bit of a torturous way of specifying it. First, you guarantee an extended lifetime based uniquely on whether the reference is bound to a temporary or not, then you eliminate all of the cases which could lead to an indirection, hoping you've gotten them all (which they probably haven't). – James Kanze Feb 06 '13 at 18:30
  • On further analysis, and considering all of the text, I'm pretty sure that this was not the intent of the committee. If so, they missed a number of cases. Consider `void f() { Foo const& f1 = Foo(); static Foo const& f2 = f1; }`. What should be the lifetime of the temporary `Foo()`. It is clearly bound to `f2`, and none of the cases following the "except" enter into consideration. There's clearly a bug in the standard. – James Kanze Feb 06 '13 at 19:01
  • @JamesKanze: Yeah, maybe. I think the _intent_ is that the lifetime is extended only when the temporary is bound to a reference and not when "a reference is bound to a reference" (yes, I know.. hence the quote marks), i.e. basically what I said in the question. It doesn't say that atm though. Worth looking into further. – Lightness Races in Orbit Feb 06 '13 at 19:19
  • I'm pretty sure that the intent is for the lifetime to be extended _only_ when the expression generating the temporary is used to initialize the reference. Otherwise, you end up with all sorts of lifetime issues, with the compiler having to do extensive analysis (and maybe introduce extra flags---in my function `f`, the first time through the temporary would have static lifetime, but the second not). The problem is formulating it correctly. I'll raise the issue with the committee, if I don't forget. – James Kanze Feb 06 '13 at 19:28
  • @JamesKanze: Yeah, that's what I meant :P I'll get Ville to mention it too if I run into him – Lightness Races in Orbit Feb 06 '13 at 19:32
7

This is subject of two issue reports, http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1299 and http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#1568 .

The former issue report, of which I am the reporter, was intended to cover all these cases where a reference is bound to a temporary object, but is not intended to be lifetime-extending. The description in the body of the issue only mentions prvalues being confused with temporary expressions (that actually decide whether lifetime of what they evaluate to is lengthened or not). But lvalue and xvalues are likewise confused with these in the Standard. An example where that happens in the context of static_cast is issue number #1568 (in which the use of "temporary variable" further confuses the matter).

Actually, this:

A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.

Contradicts the other rules in the same paragraph. Because the temporary is bound to both a reference parameter in a function call and to a local automatic reference variable.

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • Yeah, ok, you and James have convinced me that the word "except" doesn't disambiguate the paragraph's rules, because the "except" is still only being applied to a single scenario at a time, and as such cannot disambiguate two or more conflicting scenarios. – Lightness Races in Orbit Feb 07 '13 at 09:14
3

The rule which applies here is common sense. The standard is poorly worded, and does in fact guarantee this. But there's no practical way to implement it.

James Kanze
  • 150,581
  • 18
  • 184
  • 329
1

Probably I am a bit slow but to me it did not become clear what the resolution of this question is from reading the other answers. Thus I modified the code shown and wanted to summarize for others: the answer is, you get undefined behavior if you access y!

Run this code:

struct foo {
    int id;
    foo(int id) : id(id) { std::cout << "ctor " << id << std::endl; };
    ~foo() { std::cout << "dtor " << id << std::endl; }
};
const foo& func(const foo& a, const foo&) { return a; }

int main(int argc, char** argv) {
    foo x(1);
    const foo& y = func(foo(2), x);
    std::cout << "main " << y.id << std::endl;
    return 0;
}

The output for me is:

ctor 1
ctor 2
dtor 2
main 2
dtor 1

But the line main 2 is undefined behavior.

B M
  • 3,893
  • 3
  • 33
  • 47