1
votes

In C++, we can create temporary values, and these temporary values have a lifespan.

From cppreference.com:

All temporary objects are destroyed as the last step in evaluating the full-expression that (lexically) contains the point where they were created, and if multiple temporary objects were created, they are destroyed in the order opposite to the order of creation. ...

...

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

An expression could be written such that the resulting object will have dependent rvalue references. One could remove those dependencies by allocating a non-reference object and moving the temporary's contents into it, but that would be less efficient than just using the temporaries, due to the additional moves/copies.

By inserting such an expression with dependent temporary object(s) in as a function parameter, this will result in the function receiving a valid object. This would be because the expression has become a sub-expression of the full-expression.

However, if one were to extend the life of the object which is created by this same expression, the expression has now become the full-expression, so I would have expected the temporaries to live on with the final temporary in the worst case or just the dependent ones to in the best case. However, it appears that all of the intermediate temporaries are just being destroyed, resulting in a temporary with an extended lifespan with dangling references/pointers.

I believe that this issue is going to become even more relivant now that we have rvalue references available to us rather than just const references.

So my question is, why is this so? Was no use case for extending the life of dependent rvalues though of? Or was there deliberate thought behind this?

Here is an example of what I mean:

#include <iostream>

struct Y
{
    Y()  { std::cout << " Y construct\n"; }
    ~Y() { std::cout << " Y destruct\n";  }
};

struct X
{
    Y&& y;
    X(Y&& y)
        : y( (std::cout << " X construct\n",
              std::move(y)) ) {}
    ~X() { std::cout << " X destruct\n"; }
    operator Y&() { return y; }
};

void use(Y& y)
{
    std::cout << " use\n";
}

int main()
{
    std::cout << "used within fn call\n";
    use(X(Y()));
    std::cout << "\nused via life extention\n";
    auto&& x = X(Y());
    use(x);
}

Output:

used within fn call
 Y construct
 X construct
 use
 X destruct
 Y destruct

used via life extention
 Y construct
 X construct
 Y destruct
 use
 X destruct

Demo

1

1 Answers

2
votes

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.