Why Do Const References Extend the Lifetime of Rvalues

Why do const references extend the lifetime of rvalues?

It was proposed in 1993. Its purpose was to eliminate the inconsistent handling of temporaries when bound to references.

Back then, there was no such thing as RVO, so simply banning the binding of a temporary to a reference would have been a performance hit.

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/1993/N0345.pdf

Why doesn't a const reference extend the life of a temporary object passed via a function?

It's by design. In a nutshell, only the named reference to which the temporary is bound directly will extend its lifetime.

[class.temporary]

5 There are three contexts in which temporaries are destroyed at a
different point than the end of the full-expression. [...]

6 The 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 persists until the completion of the full-expression containing
    the call.
  • The lifetime of a temporary bound to the returned value in a function return statement is not extended; the temporary is destroyed
    at the end of the full-expression in the return statement.
  • [...]

You didn't bind directly to ref2, and you even pass it via a return statement. The standard explicitly says it won't extend the lifetime. In part to make certain optimizations possible. But ultimately, because keeping track of which temporary should be extended when a reference is passed in and out of functions is intractable in general.

Since compilers may optimize aggressively on the assumption that your program exhibits no undefined behavior, you see a possible manifestation of that. Accessing a value outside its lifetime is undefined, this is what return ref2; does, and since the behavior is undefined, simply returning zero is a valid behavior to exhibit. No contract is broken by the compiler.

Is it possible to extend the life of an rvalue passed into a function?

Because the rvalues are bound to the parameters of the constructor firstly, and bounding them to the class members later does not further extend their lifetime.

You can define your LazyAddition class as an aggregate to avoid binding the rvalues to parameters of constructor:

struct LazyAddition
{
const Number& lhs;
const Number& rhs;
};

But be careful. After C++20, you should use list-initialization syntax to initialize such a LazyAddition aggregate, like LazyAddition { Number(3), Number(4) }, otherwise the lifetime will still not be extended.

Returning const reference to temporary behaves differently than local const reference?

Given const Val &foo = test(Val(5));, the temporary Val(5) will be destroyed after the full expression immediately, its lifetime won't be extended to the lifteime of the reference foo. It's not bound to foo directly, but bound to the reference parameter of test.

In reference initialization,

(emphasis mine)

Whenever a reference is bound to a temporary or to a subobject
thereof, the lifetime of the temporary is extended to match the
lifetime of the reference, with the following exceptions:

  • a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if
    the function returns a reference, which outlives the full expression,
    it becomes a dangling reference.

In general, the lifetime of a temporary cannot be further extended by
"passing it on": a second reference, initialized from the reference to
which the temporary was bound, does not affect its lifetime.

Extending temporary's lifetime through rvalue data-member works with aggregate, but not with constructor, why?

TL;DR

Aggregate initialization can be used to extend the life-time of a temporary, a user-defined constructor cannot do the same since it's effectively a function call.

Note: Both T const& and T&& apply in the case of aggregate-initalization and extending the life of temporaries bound to them.



What is an Aggregate?

struct S {                // (1)
std::vector<int>&& vec;
};

To answer this question we will have to dive into the difference between initialization of an aggregate and initialization of a class type, but first we must establish what an aggregate is:

8.5.1p1 Aggregates [dcl.init.aggr]

An aggregate is an array or a class (Clause 9) with no user-provided constructors (12.1), no private or protected non-static data members (Clause 11), no base classes (Clause 10), and no virtual functions (10.3)

Note: The above means that (1) is an aggregate.



How are Aggregates initialized?

The initialization between an aggregate and a "non-aggregate" differs greatly, here comes another section straight from the Standard:

8.5.1p2 Aggregates [dcl.init.aggr]

When an aggregate is initialized by an initializer list, as specified in 8.5.4, the elements of the initializer list are taken as initializers for the members of the aggregate, in increasing subscript or member order. Each member is copy-initialized from the corresponding initializer-clause.


The above quotation states that we are initializing the members of our aggregate with the initializers in the initializer-clause, there is no step in between.

struct A { std::string a; int b; };

A x { std::string {"abc"}, 2 };


Semantically the above is equivalent to initializing our members using the below, just that A::a and A::b in this case is only accessible through x.a and x.b.

std::string A::a { std::string {"abc"} };
int A::b { 2 };


If we change the type of A::a to an rvalue-reference, or a const lvalue-reference, we will directly bind the temporary use for initialization to x.a.

The rules of rvalue-references, and const lvalue-references, says that the temporaries lifetime will be extended to that of the host, which is exactly what is going to happen.



How does initialization using a user-declared constructor differ?

struct S {                    // (2)
std::vector<int>&& vec;
S(std::vector<int>&& v)
: vec{std::move(v)} // bind to the temporary provided
{ }
};

A constructor is really nothing more than a fancy function, used to initialize a class instance. The same rules that apply to functions, apply to them.

When it comes to extending the life-time of temporaries there is no difference.

std::string&& func (std::string&& ref) {
return std::move (ref);
}


A temporary passed to func will not have its life-time extended just because we have an argument declared as being a rvalue/lvalue-reference. Even if we return the "same" reference so that it's available outside of func, it just won't happen.

This is what happens in the constructor of (2), after all a constructor is just a "fancy function" used to initialize an object.


12.2p5 Temporary objects [class.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 bound to a reference member in a constructor's ctor-initializer (12.6.2) persists until the constructor exits.

  • A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full-expression containing the call.

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


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

Note: Do note that aggregate initialization through a new T { ... } differ from the previously mentioned rules.

Why does extending the lifespan of a temporary object not result in the extension of the intermediate temporaries?

The lifetime extension rules are designed to:

  • prevent temporary objects from outliving the scope in which they are created;
  • allow the compiler to statically determine when lifetime extension occurs and when the extended lifetime ends.

Were this not so, there would be a runtime cost to lifetime extension, where every initialization of a pointer or reference would have to increment a reference count associated with the temporary object. If that's what you want, use std::shared_ptr.

In your example, the X::X(Y&&) constructor could be defined in another translation unit, so the compiler may not even be able to tell at translation time that it stores a reference to the temporary passed in. Programs are not supposed to behave differently depending on whether the function is defined in this translation unit or in another. Even if the compiler can see the definition of X::X, in principle the initializer for X::y could be an arbitrarily complex expression that may or may not actually result in an xvalue referring to the same object as the parameter y. It is not the compiler's job to attempt to decide potentially undecidable decision problems, even in special cases that are obvious to humans.

Lifetime extension of temporary by non-const reference using const-cast

Any reference can extend the lifetime of an object. However, a non-const reference cannot bind to a temporary as in your example. The Microsoft extension you refer to is not "Extend lifetime by non-const references," rather "Let non-const references bind to temporaries." They have that extension for backward compatibility with their own previous broken compiler versions.

By a cast you have forced the binding of a non-const reference to a temporary, which does not appear to be invalid, just unusual because it cannot be done directly. Once you've accomplished that binding, lifetime extension occurs for your non-const reference the same as it would for a const reference.

More information: Do *non*-const references prolong the lives of temporaries?

Does a const reference class member prolong the life of a temporary?

Only local const references prolong the lifespan.

The standard specifies such behavior in §8.5.3/5, [dcl.init.ref], the section on initializers of reference declarations. The reference in your example is bound to the constructor's argument n, and becomes invalid when the object n is bound to goes out of scope.

The lifetime extension is not transitive through a function argument. §12.2/5 [class.temporary]:

The second 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 to a subobject of which the temporary is bound persists for the lifetime of the reference except as specified below. A temporary bound to a reference member in a constructor’s ctor-initializer (§12.6.2 [class.base.init]) persists until the constructor exits. A temporary bound to a reference parameter in a function call (§5.2.2 [expr.call]) persists until the completion of the full expression containing the call.



Related Topics



Leave a reply



Submit