Prolonging the Lifetime of Temporaries

prolonging the lifetime of temporaries

I'll answer your question... the other way around.

Why did they allowed Foo const& foo = fooByValue(); to begin with ?

It makes life (somewhat) easier, but introduces potential undefined behavior all over the place.

Foo const& fooByReference()
{
return fooByValue(); // error: returning a reference to a temporary
}

This is obviously wrong, and indeed the compiler will dutifully report it. As per Tomalak's comment: it is not mandated by the standard, but good compilers should report it. Clang, gcc and MSVC do. I think that Comeau and icc would too.

Foo const& fooByIndirectReference()
{
Foo const& foo = fooByValue(); // OK, you're allowed to bind a temporary
return foo; // Generally accepted
}

This is wrong, but is more subtle. The problem is that the lifetime of the temporary is bound to the lifetime of foo, which goes out of scope at the end of the function. A copy of foo is passed to the caller, and this copy points into the ether.

I raised the bug on Clang, and Argyris was able to diagnose this case (kudos really :p).

Foo const& fooForwarder(Foo const&); // out of line implementation which forwards
// the argument

Foo const& fooByVeryIndirectReference()
{
return fooForwarder(fooByValue());
}

The temporary created by fooByValue is bound to the lifetime of the argument of fooForwarder, which dutifully provide a copy (of the reference), copy that is returned to the caller, even though it now points into the ether.

The issue here is that fooForwarder's implementation is perfectly fine wrt the standard, and yet it creates undefined behavior in its caller.

The daunting fact though, is that diagnosing this requires knowing about the implementation of fooForwarder, which is out of reach for the compiler.

The only solution I can fathom (apart from WPA) is a runtime solution: whenever a temporary is bounded to a reference, then you need to make sure that the returned reference does not share the same address... and then what ? assert ? raise an exception ? And since it's only a runtime solution, it is clearly not satisfactory.

The idea of binding a temporary to a reference is brittle.

Extending the lifetime of a temporary object without copying it

Why is the lifetime of object x not extended past the function call even if a const reference has been bound to it?

Technically, the lifetime of the object is extended past the function call. It is not however extended past the initialization of wrap. But that's a technicality.

Before we dive in, I'm going to impose a simplification: let's get rid of wrapper. Also, I'm removing the template part because it too is irrelevant:

const object &function(const object &arg)
{
return arg;
}

This changes precisely nothing about the validity of your code.

Given this statement:

const object &obj = function(object{}); // Let's call that temporary object x

What you want is for the compiler to recognize that "object x" and obj refer to the same object, and therefore the temporary's lifetime should be extended.

That's not possible. The compiler isn't guaranteed to have enough information to know that. Why? Because the compiler may only know this:

const object &function(const object &arg);

See, it's the definition of function that associates arg with the return value. If the compiler doesn't have the definition of function, then it cannot know that the object being passed in is the reference being returned. Without that knowledge, it cannot know to extend x's lifetime.

Now, you might say that if function's definition is provided, then the compiler can know. Well, there are complicated chains of logic that might prevent the compiler from knowing at compile time. You might do this:

const object *minimum(const object &lhs, const object &rhs)
{
return lhs < rhs ? lhs : rhs;
}

Well, that returns a reference to one of them, but which one will only be determined based on the runtime values of the object. Whose lifetime should be extended by the caller?

We also don't want the behavior of code to change based on whether the compiler only has a declaration or has a full definition. Either it's always OK to compile the code if it only has a declaration, or it's never OK to compile the code only with a declaration (as in the case of inline, constexpr, or template functions). A declaration may affect performance, but never behavior. And that's good.

Since the compiler may not have the information needed to recognize that a parameter const& lives beyond the lifetime of a function, and even if it has that information it may not be something that can be statically determined, the C++ standard does not permit an implementation to even try to solve the problem. Thus, every C++ user has to recognize that calling functions on temporaries if it returns a reference can cause problems. Even if the reference is hidden inside some other object.

What you want cannot be done. This is one of the reasons why you should not make an object non-moveable at all unless it is essential to its behavior or performance.

Prolonging life of a temporary object using const reference

The two links which you have referred are different in the sense that one shows the use of a local const reference and other shows the use of a class member const reference.

When we create local const references and refer to a temporary object then in this compiler extends the life of the temporary till the scope of local const reference.

Class member const reference pointing to temporary will lead to unexpected results as the life of the temporary object will not be extended beyond the constructor invoked to initialize class member reference. As explained in one of the answers the temporary will only survive till completion of the constructor.

Quoting the answer from:
Does a const reference prolong the life of a temporary?

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.

If you analyze it correctly you will realize that in both cases the life of temporary is extended till the scope from where the references are initialized is valid. As soon as the scope from where reference goes out of scope the temporary becomes invalid.

For local const reference, scope is inside a function from where it is being initialized to a temp.
For class member const reference, scope is constructor where it is being initialized to a temp.

You should also read this GOTW article:
https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/

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.

Why lifetime of temporary doesn't extend till lifetime of enclosing object?

The standard considers two circumstances under which the lifetime of a temporary is extended:

§12.2/4 There are two contexts in which temporaries are destroyed at a different point than the end of the fullexpression. The first context is when an expression appears as an initializer for a declarator defining an object. In that context, the temporary that holds the result of the expression shall persist until the object’s initialization is complete. [...]

§12.2/5 The second context is when a reference is bound to a temporary. [...]

None of those two allow you to extend the lifetime of the temporary by a later binding of the reference to another const reference. But ignore the standarese and think of what is going on:

Temporaries are created in the stack. Well, technically, the calling convention might mean that a returned value (temporary) that fits in the registers might not even be created in the stack, but bear with me. When you bind a constant reference to a temporary the compiler semantically creates a hidden named variable (that is why the copy constructor needs to be accessible, even if it is not called) and binds the reference to that variable. Whether the copy is actually made or elided is a detail: what we have is an unnamed local variable and a reference to it.

If the standard allowed your use case, then it would mean that the lifetime of the temporary would have to be extended all the way until the last reference to that variable. Now consider this simple extension of your example:

B* f() {
B * bp = new B(A());
return b;
}
void test() {
B* p = f();
delete p;
}

Now the problem is that the temporary (lets call it _T) is bound in f(), it behaves like a local variable there. The reference is bound inside *bp. Now that object's lifetime extends beyond the function that created the temporary, but because _T was not dynamically allocated that is impossible.

You can try and reason the effort that would be required to extend the lifetime of the temporary in this example, and the answer is that it cannot be done without some form of GC.

prolonging the lifetime of temporaries

I'll answer your question... the other way around.

Why did they allowed Foo const& foo = fooByValue(); to begin with ?

It makes life (somewhat) easier, but introduces potential undefined behavior all over the place.

Foo const& fooByReference()
{
return fooByValue(); // error: returning a reference to a temporary
}

This is obviously wrong, and indeed the compiler will dutifully report it. As per Tomalak's comment: it is not mandated by the standard, but good compilers should report it. Clang, gcc and MSVC do. I think that Comeau and icc would too.

Foo const& fooByIndirectReference()
{
Foo const& foo = fooByValue(); // OK, you're allowed to bind a temporary
return foo; // Generally accepted
}

This is wrong, but is more subtle. The problem is that the lifetime of the temporary is bound to the lifetime of foo, which goes out of scope at the end of the function. A copy of foo is passed to the caller, and this copy points into the ether.

I raised the bug on Clang, and Argyris was able to diagnose this case (kudos really :p).

Foo const& fooForwarder(Foo const&); // out of line implementation which forwards
// the argument

Foo const& fooByVeryIndirectReference()
{
return fooForwarder(fooByValue());
}

The temporary created by fooByValue is bound to the lifetime of the argument of fooForwarder, which dutifully provide a copy (of the reference), copy that is returned to the caller, even though it now points into the ether.

The issue here is that fooForwarder's implementation is perfectly fine wrt the standard, and yet it creates undefined behavior in its caller.

The daunting fact though, is that diagnosing this requires knowing about the implementation of fooForwarder, which is out of reach for the compiler.

The only solution I can fathom (apart from WPA) is a runtime solution: whenever a temporary is bounded to a reference, then you need to make sure that the returned reference does not share the same address... and then what ? assert ? raise an exception ? And since it's only a runtime solution, it is clearly not satisfactory.

The idea of binding a temporary to a reference is brittle.

Ternary operator and prolonging the lifetime of a temporary object via reference-to-const

No, it's not well-behaved. For the lifetime extension to occur, the temporary must be bound directly to the reference. Once you add a cast, the binding is no longer direct, and the resulting reference becomes dangling.

We can see this with some simple testing code:

#include <iostream>
struct Gizmo
{
~Gizmo() { std::cout << "Gizmo destroyed\n"; }
};

Gizmo Frobnicate(const Gizmo& arg) { return arg; }

void ProcessGizmo(const Gizmo& arg, bool frobnicate)
{
const Gizmo& local = frobnicate ? static_cast<const Gizmo&>(Frobnicate(arg)) : arg;
// Perform some work on local
(void) local;
std::cout << "Processing\n";
}

int main(){
Gizmo g;
ProcessGizmo(g, true);
std::cout << "Processed\n";
}

This prints:

Gizmo destroyed
Processing
Processed
Gizmo destroyed

That first Gizmo destroyed message is from the return value of Frobnicate(); it's being destroyed at the end of that line - without lifetime extension.

An obvious workaround is moving the processing into another function:

void DoProcessGizmo(const Gizmo& arg) { /* process the Gizmo */ }

void ProcessGizmo(const Gizmo& arg, bool frobnicate)
{
return frobnicate ? DoProcessGizmo(Frobnicate(arg)) : DoProcessGizmo(arg);
}

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.



Related Topics



Leave a reply



Submit