Does a constant reference member variable in an anonymous struct extend the lifetime of a temporary?
Asked Answered
V

2

20

Consider the following code:

struct Temp{ int i = 0; };

Temp GetTemp() { return Temp{}; }

int main()
{
    for (struct{Temp const & t; int counter;} v = {GetTemp(), 0};
        v.counter < 10; 
        ++v.counter) 
    {
      // Is v.t guaranteed to not be a dangling reference?
      std::cout << v.t.i << std::endl;
    }
}

So GetTemp() returns a temporary object, which then gets assigned to a constant reference variable. However, that constant reference variable is a member of an anonymous local struct. Question: Does the C++ standard guarantee that the lifetime of that temporary gets extended till after the loop terminates?

Considering this question, I would have expected the answer to be no, i.e. that I get a dangling reference in the loop body. However, gcc and clang seem to extend the lifetime (see example on godbolt), and even do not complain with -fsanitize=undefined, which surprised me.

Vintner answered 20/1, 2023 at 12:59 Comment(5)
Why did you choose to go with an anonymous structure here? I'm genuinely interested as I think this question would be equally valid with a normally declared structure. and the anonymous structure just adds complexity.Estivate
Is there a specific reason you tagged this C++14? The question would equally make sense with any standard.Elwira
The background for this question is a bug that I had to fix in a external library with horrible code. It defines foreach(x,c) macros, which expand to for loops. x is the loop variable, c the container. The macro definition uses c directly multiple times. The library sometimes calls it like foreach(x,Get temp()), meaning the GetTemp is called multiple times. I looked for ways to store the temporary properly in a local variable. But I can't just do auto&& t=c; in the macro definition before the for loop, as this would break in e.g. if(...) foreach(...){} due to missing if-braces.Vintner
I tagged the question c++14 since the external library where the question arose is compiled with c++14, and I wasn't sure whether something relevant changed in the various c++ versions due to the use of the braced initialization.Vintner
Duplicate Question.Fumed
E
12

For braced aggregate initialization as in your example, lifetime extension has been guaranteed since C++98 (irrespective of the linkage/visibility properties of the class). This is intuitive, since the reference is directly bound to the temporary, and not via some intermediate ctor parameter, as in the question you've linked. For legalese, see [class.temporary] in the C++14 FD that outlines the lifetime extension contexts.

See also the note here which differentiates braced and parenthetical initialization since C++20:

[Note 7: By contrast with direct-list-initialization, narrowing conversions ([dcl.init.list]) are permitted, designators are not permitted, a temporary object bound to a reference does not have its lifetime extended ([class.temporary]), and there is no brace elision.
[Example 3:

struct A {
    int a;
    int&& r;
};

int f();
int n = 10;

A a1{1, f()};                   // OK, lifetime is extended 
A a2(1, f());                   // well-formed, but dangling reference
Elwira answered 20/1, 2023 at 13:13 Comment(1)
Comments are not for extended discussion; this conversation has been moved to chat.Dialyser
M
5

Does a constant reference member variable in an anonymous struct extend the lifetime of a temporary?

Yes, the chain of const references is unbroken. There's is only v and v is alive until the end of the for loop and the lifetime of the referenced Temp is therefore extended until then. The fact that the struct is anonymous has no impact.

class.temporary/4

There are two contexts in which temporaries are destroyed at a different point than the end of the full-expression. The first context is when a default constructor is called to initialize an element of an array. If the constructor has one or more default arguments, the destruction of every temporary created in a default argument is sequenced before the construction of the next array element, if any.

class.temporary/5

The second context is when a reference is bound to a temporary. 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:

  • (5.1) A temporary bound to a reference member in a constructor's ctor-initializer ([class.base.init]) persists until the constructor exits.
  • (5.2) A temporary bound to a reference parameter in a function call ([expr.call]) persists until the completion of the full-expression containing the call.
  • (5.3) The lifetime of a temporary bound to the returned value in a function return statement ([stmt.return]) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.
  • (5.4) A temporary bound to a reference in a new-initializer ([expr.new]) persists until the completion of the full-expression containing the new-initializer.

None of the exceptions to the lifetime extension applies to your case.

Marketable answered 20/1, 2023 at 14:16 Comment(4)
Can you clarify what "Yes, the chain of const references is unbroken." means? The question the author cites also has a chain of const& if you so will, but that behaves differently.Elwira
@Elwira I mean that one could do it in steps: const& a = tmp; const& b = a; const& c = b; ... - and that would be an unbroken chain.Marketable
You do realize that doing that would never extend the lifetime of tmp beyond that of a?Elwira
@Elwira Jeez, I've tried to write some nice examples of broken and unbroken const& chains but it just becomes messy :) I'll have to give that another go Tomorrow (or I'll just remove that sentence - it doesn't appear anywhere in the standard anyway so perhaps that would be the best).Marketable

© 2022 - 2024 — McMap. All rights reserved.