13
votes

Given this code sample, what are the rules regarding the lifetime of the temporary string being passed to S.

struct S
{
    // [1] S(const std::string& str) : str_{str} {}
    // [2] S(S&& other) : str_{std::move(other).str} {}

    const std::string& str_;
};

S a{"foo"}; // direct-initialization

auto b = S{"bar"}; // copy-initialization with rvalue

std::string foobar{"foobar"};
auto c = S{foobar}; // copy-initialization with lvalue

const std::string& baz = "baz";
auto d = S{baz}; // copy-initialization with lvalue-ref to temporary

According to the standard:

N4140 12.2 p5.1 (removed in N4296)

A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

N4296 12.6.2 p8

A temporary expression bound to a reference member in a mem-initializer is ill-formed.

So having a user defined constructor like [1] is definitively not what we want. It's even supposed to be ill-formed in the latest C++14 (or is it?) neither gcc nor clang warned about it.
Does it change with direct aggregate initialization? I looks like in that case, the temporary lifetime is extended.

Now regarding copy-initialization, Default move constructor and reference members states that [2] is implicitly generated. Given the fact that the move might be elided, does the same rule apply to the implicitly generated move constructor?

Which of a, b, c, d has a valid reference?

1
There is no exception from the lifetime extensions of temporaries for aggregate initialization, hence the lifetime of the temporary will get extended. This guarantees a proper lifetime for the temporary created in the case "direct-initialization".dyp
what do you mean "the move might be elided" ? Reference binding can't be elided, str_ binds directly to other.str. (the std::move has no effect)M.M
@M.M I mean most compilers will perform a direct initialization rather than using the move constructor. Yes std::move(other).str is the same as other.str for references and has no effect here3XX0
The change that made binding temporaries to reference members in mem-initializers ill-formed was done because of CWG 1696, which is post-C++14 (status: DRWP). Its implementation status in clang is "unknown". Not sure if such a list exists for gcc.dyp

1 Answers

5
votes

The lifetime of temporary objects bound to references is extended, unless there's a specific exception. That is, if there is no such exception, then the lifetime will be extended.

From a fairly recent draft, N4567:

The second context [where the lifetime is extended] 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 object bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.
  • (5.2) The lifetime of a temporary bound to the returned value in a function return statement (6.6.3) is not extended; the temporary is destroyed at the end of the full-expression in the return statement.
  • (5.3) A temporary bound to a reference in a new-initializer (5.3.4) persists until the completion of the full-expression containing the new-initializer.

The only significant change to C++11 is, as the OP mentioned, that in C++11 there was an additional exception for data members of reference types (from N3337):

  • A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits.

This was removed in CWG 1696 (post-C++14), and binding temporary objects to reference data members via the mem-initializer is now ill-formed.


Regarding the examples in the OP:

struct S
{
    const std::string& str_;
};

S a{"foo"}; // direct-initialization

This creates a temporary std::string and initializes the str_ data member with it. The S a{"foo"} uses aggregate-initialization, so no mem-initializer is involved. None of the exceptions for lifetime extensions apply, therefore the lifetime of that temporary is extended to the lifetime of the reference data member str_.


auto b = S{"bar"}; // copy-initialization with rvalue

Prior to mandatory copy elision with C++17: Formally, we create a temporary std::string, initialize a temporary S by binding the temporary std::string to the str_ reference member. Then, we move that temporary S into b. This will "copy" the reference, which will not extend the lifetime of the std::string temporary. However, implementations will elide the move from the temporary S to b. This must not affect the lifetime of the temporary std::string though. You can observe this in the following program:

#include <iostream>

#define PRINT_FUNC() { std::cout << __PRETTY_FUNCTION__ << "\n"; }

struct loud
{
    loud() PRINT_FUNC()
    loud(loud const&) PRINT_FUNC()
    loud(loud&&) PRINT_FUNC()
    ~loud() PRINT_FUNC()
};

struct aggr
{
    loud const& l;
    ~aggr() PRINT_FUNC()
};

int main() {
    auto x = aggr{loud{}};
    std::cout << "end of main\n";
    (void)x;
}

Live demo

Note that the destructor of loud is called before the "end of main", whereas x lives until after that trace. Formally, the temporary loud is destroyed at the end of the full-expression which created it.

The behaviour does not change if the move constructor of aggr is user-defined.

With mandatory copy-elision in C++17: We identify the object on the rhs S{"bar"} with the object on the lhs b. This causes the lifetime of the temporary to be extended to the lifetime of b. See CWG 1697.


For the remaining two examples, the move constructor - if called - simply copies the reference. The move constructor (of S) can be elided, of course, but this is not observable since it only copies the reference.