Veritas
Veritas

Reputation: 2210

Member rvalue references and object lifetime

It's been a while since I last looked at temporary lifetime rules and I don't remember how member rvalue references affect lifetime.

For example take the two following pieces of code:

int main() 
{
    std::get<0>(std::forward_as_tuple(
        [](int v){ std::cout << v << std::endl; }
    ))(6);
    return 0;
}

,

int main() 
{
    auto t = std::forward_as_tuple(
        [](int v){ std::cout << v << std::endl; }
    );
    std::get<0>(t)(6);
    return 0;
}

If member rvalue references don't affect lifetime rules, I would expect the first example to be well-behaved while the second example to be undefined (since the full-expression containing the lambda object ends at the first semicolon).

How do C++11, C++14 and C++17 treat the given examples? Are there differences between the three?

Upvotes: 2

Views: 938

Answers (3)

Barry
Barry

Reputation: 302767

Because this is a question. The rules for lifetime extension are in [class.temporary]. The wording from C++11 to C++14 to C++17 didn't change in a way that is relevant to this particular question. The rule is:

There are [two/three] contexts in which temporaries are destroyed at a different point than the end of the full-expression. The first context is when a default constructor is called to initialize an element of an array [...]

The [second/third] context 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:
— 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.

This expression:

std::forward_as_tuple([](int v){ std::cout << v << std::endl; })

involves binding a reference (the parameter in forward_as_tuple) to a prvalue (the lambda-expression), which is explicitly mentioned in C++11/14 as a context which creates a temporary:

Temporaries of class type are created in various contexts: binding a reference to a prvalue, [...]

which in C++17 is worded as:

Temporary objects are created
(1.1) — when a prvalue is materialized so that it can be used as a glvalue (4.4),

Either way, we have a temporary, it's bound to a reference in a function call, so the temporary object persists until the completion of the full-epxression containing the call.

So this is okay:

std::get<0>(std::forward_as_tuple(
    [](int v){ std::cout << v << std::endl; }
))(6);

But this calls through a dangling reference:

auto t = std::forward_as_tuple(
    [](int v){ std::cout << v << std::endl; }
);
std::get<0>(t)(6);

because the lifetime of the temporary function object ended at the end of the statement initializing t.

Note that this has nothing to do with member rvalue references. If we had something like:

struct Wrapper {
    X&& x;
};

Wrapper w{X()};

then the lifetime of the temporary X persists through the lifetime of w, and w.x isn't a dangling reference. But that's because there's no function call.


C++17 introduced a 3rd context which involves copying an array, which is unrelated here.

Upvotes: 4

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275340

Lifetime extension applies when you directly bind a temporary to a reference anywhere but within a constructor initializer list. (Note: aggregate initialization isn't a constructor)

std::forward_as_tuple is a function. Any temporary passed to it cannot be lifetime extended beyond the current line.

Temporaries by default last until the end of the current line, basically. (What exactly that spot is isn't actually the end of the current line). In your two cases, it is the end of the current line (the ;) where the temporary ends its lifetime. This is long enough for the first case; in the second case, the temporary is dead and your code exhibits undefined behavior.

At the same time:

struct foo {
  int&& x;
};

int main() {
  foo f{3};
  std::cout << f.x << "\n";
}

is perfectly well defined. No constructor, we bound a temporary to an (rvalue) reference, thus lifetime is extended.

Add this:

struct foo {
  int&& x;
  foo(int&& y):x(y) {}
};

or

struct foo {
  int&& x;
  foo(int y):x((int)y) {}
};

and it is now UB.

The first one because we bound the temporary to an rvalue reference when invoking the ctor. The inside of the constructor is irrelevant, as there is no temporary being bound directly there. Then the argument to the function and the temporary both go out of scope in main.

The second because the rule that binding the temporary (int)y0 to int&&x in a constructor initializer list does not extend lifetime the same way as it does elsewhere.

Upvotes: 6

Nicol Bolas
Nicol Bolas

Reputation: 473292

The rules of temporary lifetime extension haven't changed in any version of C++ since 98. We may have new ways to manifest temporaries, but once they exist, their lifetimes are well understood.

It should be noted that your example doesn't really apply to member references of any kind. You're calling a function with a temporary, and the parameter is a forwarding reference. The temporary in question is therefore bound to the function parameter reference, not to a member reference. The lifetime of that temporary will end after the expression that invoked that function, as it would with any temporary passed to a reference parameter.

The fact that this function (forward_as_tuple) will eventually store that reference in a tuple is irrelevant. What you do with a reference cannot alter its lifetime.

Again, that's C++98, and none of the later versions changed that.

Upvotes: 5

Related Questions