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::
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.