Vincent
Vincent

Reputation: 60371

Extending the lifetime of a temporary object without copying it

Consider the following code:

#include <utility>
#include <iostream>

struct object {
    object(const object&) = delete;
    object(object&&) = delete;
    object() {std::clog << "object::object()\n";}
    ~object() {std::clog << "object::~object()\n";}
    void operator()() const {std::clog << "object::operator()()\n";}
};

struct wrapper {
    const object& reference;
    void operator()() const {reference();}
};

template <class Arg>
wrapper function(Arg&& arg) {
    wrapper wrap{std::forward<Arg>(arg)};
    return wrap;
}

int main(int argc, char* argv[]) {
    wrapper wrap = function(object{}); // Let's call that temporary object x
    wrap();
    return 0;
}

I am really surprised that it prints:

object::object()
object::~object()
object::operator()()

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

Question 2: Is there any way to implement the wrapper so that it would extend the lifetime of x past the function call?

Note: The copy and move constructors of the object have been explicitly deleted to make sure only one instance of it exists.

Upvotes: 2

Views: 1098

Answers (2)

Nicol Bolas
Nicol Bolas

Reputation: 473302

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.

Upvotes: 4

Phil1970
Phil1970

Reputation: 2623

As far as I know, the only case the lifetime if extended if for the return value of a function,

struct A { int a; };
A f() { A a { 42 }; return a`}
{
    const A &r = f(); // take a reference to a object returned by value
    ...
    // life or r extended to the end of this scope
}

In your code, you pass the reference to the "constructor" of A class. Thus it is your responsability to ensure that the passed object live longer. Thus, your code above contains undefined behavior.

And what you see would probably be the most probable behavior in a class that do not make reference to any member. If you would access object member (including v-table), you would most likely observe a violation access instead.

Having said that, the correct code in your case would be:

int main(int argc, char* argv[]) 
{
    object obj {};
    wrapper wrap = function(obj);
    wrap();
    return 0;
}

Maybe what you want is to move the temporary object into the wrapper:

struct wrapper {
    wrapper(object &&o) : obj(std::move(o)) {}
    object obj;
    void operator()() const {obj();}
};

In any case, the original code does not make much sense because it is build around false assumption and contains undefined behavior.

The life of a temporary object is essentially the end of the expression in which it was created. That is, when processing wrap = function(object{}) is completed.

So in resume:

Answer 1 Because you try to apply lifetime extension to a context other that the one specified in the standard.

Answer 2 As simple as moving the temporary object into a permanent one.

Upvotes: 0

Related Questions