GCC vs. Clang on the lifetime of temporary bound to an rvalue reference of another temporary

Viewed 80

I want to figure out the lifetime of a temporary object S{} bound to an rvalue reference inside struct wrap<T>. wrap<T>::f() is a function that potentially interacts with the temporary; therefore, S{} must be alive when wrap<T>::f() is called. I consider two cases: (1) wrap{S{}}.f(); and (2) auto w = wrap{S{}}; w.f();. I am confident the first case should not result in any dangling references because S{} is destroyed only after the entire expression is evaluated. However, for the second case, I am not really certain. I hypothesized that the lifetime of the temporary should match the lifetime of w. To test my hypothesis, I wrote the following code snippet (also available on Compiler Explorer):

#include <stdio.h>

struct S {
    S() { puts("S::S()"); }
    ~S() { puts("S::~S()"); }
};

template <typename T>
struct wrap {
    T &&t;

    void f() {
        puts("wrap<T>::f()");
    }
};

template <typename T>
wrap(T &&) -> wrap<T>;

int main() {
    #if defined(_MSC_VER)
    puts("msvc");
    #elif defined(__clang__)
    puts("clang");
    #else // gnu
    puts("gcc");
    #endif

    puts("== first ==");
    wrap{S{}}.f();
    
    puts("== second ==");
    auto w = wrap{S{}};
    w.f();
    return 0;
}

It seems like there is a disagreement among the compilers:

For x86-64 GCC 12.1 with -std=c++20:

gcc
== first ==
S::S()
wrap<T>::f()
S::~S()
== second ==
S::S()
wrap<T>::f()
S::~S()

For x86-64 clang (trunk) with -std=c++20:

clang
== first ==
S::S()
wrap<T>::f()
S::~S()
== second ==
S::S()
S::~S()
wrap<T>::f()

And, finally, x64 msvc v19.latest with /std:c++latest:

msvc
== first ==
S::S()
wrap<T>::f()
S::~S()
== second ==
S::S()
wrap<T>::f()
S::~S()

GCC and MSVC extend the lifetime of the temporary object, whereas clang does not. Which behavior is correct according to the standard?

1 Answers

clang is incorrect: both examples are valid and within lifetimes.

The rule is that temporaries bound to references live as long as the reference (with some exceptions that don't apply here). Note that:

auto w = wrap{S{}};

is exactly equivalent to:

wrap<S> w{S{}};

And list-initialization directly binds (as opposed to parentheses).


There is even an example in the standard that is basically exactly this, in [class.temporary]:

struct S {
  const int& m;
};
const S& s = S{1};  // both S and int temporaries have lifetime of s
Related