Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Aggregate reference member and temporary lifetime

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?

like image 413
3XX0 Avatar asked Feb 10 '16 10:02

3XX0


People also ask

When does the lifetime of a reference begin and end?

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.

What is the lifetime of an object and reference?

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

When does the lifetime of non static data members begin and end?

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:

How do I extend the lifetime of a temporary object?

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.


1 Answers

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.

like image 91
dyp Avatar answered Nov 13 '22 14:11

dyp