36

A friend of mine showed me a program in C++20:

#include <iostream>

struct A
{
    A() {std::cout << "A()\n";}
    ~A() {std::cout << "~A()\n";}
};

struct B
{
    const A &a;
};

int main()
{
    B x({});
    std::cout << "---\n";
    B y{{}};
    std::cout << "---\n";
    B z{A{}};
    std::cout << "---\n";
}

In GCC it prints:

A()
~A()
---
A()
---
A()
---
~A()
~A()

https://gcc.godbolt.org/z/ce3M3dPeo

So the lifetime of A is prolonged in cases y and z.

In Visual Studio the result is different:

A()
~A()
---
A()
---
A()
~A()
---
~A()

So the lifetime of A is only prolonged in case y.

Could you please explain why the type of braces influences the object lifetime?

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
Fedor
  • 17,146
  • 13
  • 40
  • 131
  • 2
    I believe this is called undefined behavior. meaning, for all three you should not assume that the lifetime of A was extended. However the compiler may optimize away B as an intermediate step, leaving you with a construction that does extend the lifetime of A. – Patrick Parker Jul 10 '21 at 09:55
  • 5
    @PatrickParker In some cases the lifetime extension can be guaranteed. Even when it's not, it wouldn't be UB until you read/write to the reference. – HolyBlackCat Jul 10 '21 at 10:35

1 Answers1

31

Gcc is correct. The lifetime of the temporary will be extended only when using list-initialization syntax (i.e. using braces) in initialization of an aggregate.

(since C++20) a temporary bound to a reference in a reference element of an aggregate initialized using direct-initialization syntax (parentheses) as opposed to list-initialization syntax (braces) exists until the end of the full expression containing the initializer.

struct A {
  int&& r;
};
A a1{7}; // OK, lifetime is extended
A a2(7); // well-formed, but dangling reference

For direct initialization:

(emphasis mine)

otherwise, if the destination type is a (possibly cv-qualified) aggregate class, it is initialized as described in aggregate initialization except that narrowing conversions are permitted, designated initializers are not allowed, a temporary bound to a reference does not have its lifetime extended, there is no brace elision, and any elements without an initializer are value-initialized. (since C++20)

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • 6
    IIRC the intent (behind `()` not extending lifetime) was to mimic the behavior of user-provided constructors, which can't extend lifetime, because they initialize reference members indirectly (from the reference parameters), and lifetime extension doesn't propagate when initializing one reference with the other. – HolyBlackCat Jul 10 '21 at 10:09
  • What about z-case above? Is MSVC wrong there? Please note that Clang and GCC also behave differently in a similar situation: https://gcc.godbolt.org/z/KP3c6cv8Y – Fedor Jul 11 '21 at 19:56
  • 1
    @Fedor Lifetime should be extended in z-case, it's same as y-case mainly, I think it's MSVC's bug. I'm not sure about `B b{ B{ A{} } };` which might deserve a new question, but I tend to think Clang is right; [*In general, the lifetime of a temporary cannot be further extended by "passing it on": a second reference, initialized from the reference variable or data member to which the temporary was bound, does not affect its lifetime.*](https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary) – songyuanyao Jul 12 '21 at 01:47
  • Related question: https://stackoverflow.com/questions/68335315/what-happens-when-mandatory-rvo-is-applied-to-a-reference-thats-extending-the-l – Fedor Jul 12 '21 at 07:14