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?
The lifetime of a reference begins when its initialization is complete and ends as if it were a scalar object. Note: the lifetime of the referred object may end before the end of the lifetime of the reference, which makes dangling references possible.
Every object and reference has a lifetime, which is a runtime property: for any object or reference, there is a point of execution of a program when its lifetime begins, and there is a moment when it ends. storage with the proper alignment and size for its type is obtained, and
Lifetimes of non-static data members and base subobjects begin and end following class initialization order . Temporary objects are created when a prvalue is materialized so that it can be used as a glvalue, which occurs (since C++17) in the following situations:
The lifetime of a temporary object may be extended by binding to a const lvalue reference or to an rvalue reference (since C++11), see reference initialization for details.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With