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.