The Lifetime of A Temporary and Its Extension: Explained

In this post, we are going to discuss the lifetime of a temporary object, how to extend the lifetime with a reference, and also the exceptions when the extension cannot be applied.


About The Code

A piece of sample code is worth a thousand words. This post contains sample code to help you better understand the somewhat abstruse wording of the C++ standard. The full source is located on my GitHub. In the source directory, you will find only one CPP file and a CMake file. You can compile it with CMake or directly with any c++ compilers of your choice. Following is the command I use to compile it with GCC.

g++ lifetime_of_a_temporary.cpp -o lifetime_of_a_temporary -std=c++14 -g

I have tested the sample code using GCC 7.3.0 on Ubuntu 18.04.1 LTS with C++11 and C++14. They all give the same result.


Helper Functions

Before we get started, let us first look at the helper functions, or macros, I use to better present the outcomes.

#define TRACE_FUNCTION_CALL(){                                      \
    std::cout << "Inside:  " << __PRETTY_FUNCTION__ << std::endl;   \
}

TRACE_FUNCTION_CALL() macro is just a convenient way to show the name of the current function.

#define TRACED_LOCAL_SCOPE(name)                                    \
    for(auto flag = true;                                           \
        flag and std::cout << "Scope Begin (" << name << ") {\n";   \
        flag = false,                                               \
        std::cout << "} End Scope\n\n" << std::endl)

TRACED_LOCAL_SCOPE(name) is a tricky use of the for loop syntax to trace the begin and the end of a local scope. When it is used like this:

TRACED_LOCAL_SCOPE(name){
    //some code
}

Which is equivalent to something like this:

std::cout << "Scope Begin (" << name << ") {\n";
{
    //some code
}
std::cout << "} End Scope\n\n" << std::endl;


Lifetime of a Temporary

Normally, a temporary object lasts only until the end of the full expression in which it appears. [1]

struct Base {
    Base() {
        TRACE_FUNCTION_CALL();
    }
    ~Base() {
        TRACE_FUNCTION_CALL();
    }
};

int main() {
    TRACED_LOCAL_SCOPE("a temporary") {
        Base{};
        TRACE_FUNCTION_CALL();
    }
}

The output looks like this:

Scope Begin (a temporary) {
Inside:  Base::Base()
Inside:  Base::~Base()
Inside:  int main()
} End Scope

As you can see, it first enters the local scope. Then construct the temporary Base object. After that, the temporary object is destructed immediately, before reaching the line that prints out the name of the main() function. Finally, the scope ends.


Extend the Lifetime of a Temporary

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. [2]

With the same Base struct, but this time we bind the temporary object to a const lvalue reference:

    TRACED_LOCAL_SCOPE("const lvalue reference") {
        const auto &b = Base{};
        TRACE_FUNCTION_CALL();
    }

Note: the code is still wrapped inside main(), but that part is omitted in the rest of this article to save space. Again, for the full working example, see here.
Now the output:

Scope Begin (const lvalue reference) {
Inside:  Base::Base()
Inside:  int main()
Inside:  Base::~Base()
} End Scope

The difference is very clear, when you compare it with the previous example. This time, after binding to a const lvalue reference, the temporary object is destructed after printing the name of main(), and right before the end of the local scope.
This lifetime extension of a temporary object, also works with rvalue reference.

    TRACED_LOCAL_SCOPE("rvalue reference") {
        auto &&b = Base{};
        TRACE_FUNCTION_CALL();
    }

Gives the exact same result as the const lvalue reference example.

An interesting aspect of the this feature is that when the reference does go out of scope, the same destructor that would be called for the temporary object will get called, even if it is bound to a reference to its base class type. [1]

struct Derived : public Base {
    Derived() {
        TRACE_FUNCTION_CALL();
    }
    ~Derived() {
        TRACE_FUNCTION_CALL();
    }
};

    TRACED_LOCAL_SCOPE("Derived") {
        const Base &b = Derived{};
        TRACE_FUNCTION_CALL();
    }

The output:

Scope Begin (Derived) {
Inside:  Base::Base()
Inside:  Derived::Derived()
Inside:  int main()
Inside:  Derived::~Derived()
Inside:  Base::~Base()
} End Scope

It has to be pointed out that neither struct Base or Derived has a virtual desctructor. And when the lifetime of a temporary Derived object, which binds to a Base reference, end, the right destructor Derived::~Derived() gets called.


Exceptions

There are some exceptions to this rule where the lifetime of a temporary object cannot be extended.

Exception 1

a temporary bound to a return value of a function in a return statement is not extended: it is destroyed immediately at the end of the return expression. Such function always returns a dangling reference. [2]

const Base &FunctionException1() {
    TRACE_FUNCTION_CALL();
    return Base{};
}

    TRACED_LOCAL_SCOPE("Exception 1") {
        const auto &b = FunctionException1();
        TRACE_FUNCTION_CALL();
    }

GCC will also warn you for this.

warning: returning reference to temporary [-Wreturn-local-addr]

Output of this example:

Scope Begin (Exception 1) {
Inside:  const Base& {anonymous}::FunctionException1()
Inside:  Base::Base()
Inside:  Base::~Base()
Inside:  int main()
} End Scope


Exception 2

a temporary bound to a reference member in a constructor initializer list persists only until the constructor exits, not as long as the object exists. (note: such initialization is ill-formed as of DR 1696). [2]

It also has a mark says “until C++14”. But this doesn’t mean this exception is gone. Rather it has been replaced with an improved version and moved to other places. Please refer to CWG 1696 for more details. One case of this exception is N4800 § 10.9.2 [class.base.init] paragraph 8:

A temporary expression bound to a reference member in a mem-initializer is ill-formed.

struct DerivedWrapper {
    const Derived &d;
    DerivedWrapper(): d{} {
        TRACE_FUNCTION_CALL();
    }
    ~DerivedWrapper() {
        TRACE_FUNCTION_CALL();
    }
};

    TRACED_LOCAL_SCOPE("Exception 2") {
        DerivedWrapper dw;
        TRACE_FUNCTION_CALL();
    }

will print:

Scope Begin (Exception 2) {
Inside:  Base::Base()
Inside:  Derived::Derived()
Inside:  DerivedWrapper::DerivedWrapper()
Inside:  Derived::~Derived()
Inside:  Base::~Base()
Inside:  int main()
Inside:  DerivedWrapper::~DerivedWrapper()
} End Scope

Apparently, if the lifetime of the temporary Derived object were extended, its destructor should be called after DerivedWrapper::~DerivedWrapper() get called.
GCC doesn’t warn you by default. But you can see the warning message if you compile the code with the -Wextra flag to turn on extra warnings.

warning: a temporary bound to ‘DerivedWrapper::d’ only persists until the constructor exits [-Wextra]


Exception 3

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. [2]

const Base &FunctionException3(const Base &b) {
    TRACE_FUNCTION_CALL();
    return b;
}

    TRACED_LOCAL_SCOPE("Exception 3") {
        const auto &b = FunctionException3({});
        TRACE_FUNCTION_CALL();
    }

Here, the output:

Scope Begin (Exception 3) {
Inside:  Base::Base()
Inside:  const Base& {anonymous}::FunctionException3(const Base&)
Inside:  Base::~Base()
Inside:  int main()
} End Scope

The reference parameter does extend the lifetime of the temporary Base object until the end of the function FunctionException3(), but not after the function returns.
I don’t think GCC has a warning for this exception, so if you know how to trigger the warning, please leave comments.


Exception 4

a temporary bound to a reference in the initializer used in a new-expression exists until the end of the full expression containing that new-expression, not as long as the initialized object. If the initialized object outlives the full expression, its reference member becomes a dangling reference. [2]

One example of this exception is:

struct BaseWrapper {
    const Base &b;
};

    TRACED_LOCAL_SCOPE("Exception 4") {
        auto *w = new BaseWrapper{{}};
        TRACE_FUNCTION_CALL();
        delete w;
    }

And, the output on my machine:

Scope Begin (Exception 4) {
Inside:  Base::Base()
Inside:  Base::~Base()
Inside:  int main()
} End Scope

Notice, the Base destructor get called before printing the name of main(), and well before “delete w“.


A Special Case

The following case may be considered quite special, or not, when you compare it with the Exception 4.

    TRACED_LOCAL_SCOPE("Special Case") {
        BaseWrapper w{{}};
        TRACE_FUNCTION_CALL();
    }

shows:

Scope Begin (Special Case) {
Inside:  Base::Base()
Inside:  int main()
Inside:  Base::~Base()
} End Scope

Basically, this means that you CAN extend the lifetime of a temporary object by binding it to a reference that is a member of an object.


Summary

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. [2]
It is also worth to point out that binding a temporary array to an instance of std::initializer_list works much like binding a temporary object to a reference. [3]




References

[1] GotW #88: A Candidate For the “Most Important const”.
[2] Reference initialization.
[3] The cost of std::initializer_list.
[4] Stack Overflow: Aggregate reference member and temporary lifetime.

4 thoughts on “The Lifetime of A Temporary and Its Extension: Explained

  1. A special case does not work in the following example, any idea why?

    class A{
    public:
    const int &val;
    public:
    A(const int &param): val(param)
    {
    cout<<“\nInside the construtor , val = “<<val;
    }

    };
    int main() {
    A obj(4);
    cout<<“\nValue of mem val = “<<obj.val;
    return 0;
    }

    • The problem is with class A’s constructor, when A has a constructor like that, it is no longer the special case anymore, but belongs to the Exception 3 now.

  2. thank you for your reply. But I am confused as if why Special case Works but Exception 4 does not. Why would c++ committee provide this confusing exception? We are just making one object on the heap while another one on the stack. can you share your views on the same?

    • The special case works because of “copy elision”. The temporary object is constructed directly “in place”, so there is no “copy” of the reference, thus the lifetime of the temporary object could be extended.

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s