The Lifetime of A Temporary and Its Extension: Explained

A blog about C++ programming, more or less.

You probably already known that, in C++, every temporary object has a lifetime, which begins when it is created and ends when it is destroyed automatically. However, do you know you can extend temporary objects’ lifetime with a reference? In this post, we are going to discuss the lifetime of temporary objects, how to extend their lifetime with a reference, and also the exceptions when that extension cannot be applied.

Sample 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. You can compile it with CMake or directly with any c++ compilers of your choice. Following is the command I used to compile it with GCC.

g++ a-temporary.cpp -o 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 function

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

// trace.hpp

#pragma once

#include <iostream>

// clang-format off
#define TRACE_FUNCTION_CALL() {                     \
    std::cout << __PRETTY_FUNCTION__ << std::endl;  \
}
// clang-format on

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

Lifetime of a temporary

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

// base.hpp

#pragma once

#include "trace.hpp"

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

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

// a-temporary.cpp

#include "base.hpp"

int main() {
    Base {};
    TRACE_FUNCTION_CALL();
}

The output looks like this:

$ ./a-temporary
Base::Base()
Base::~Base()
int main()

As you can see, it first constructs 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.

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:

// const-lvalue-reference.cpp

#include "base.hpp"

int main() {
    const auto &b = Base {};
    TRACE_FUNCTION_CALL();
}

Now the output becomes:

$ ./const-lvalue-reference
Base::Base()
int main()
Base::~Base()

The difference is pretty 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().

Of course, this lifetime extension of a temporary object, also works with rvalue reference.

// rvalue-reference.cpp

#include "base.hpp"

int main() {
    auto &&b = Base {};
    TRACE_FUNCTION_CALL();
}

This example gives the same result as the const lvalue reference one.

An interesting aspect of 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]

// derived.hpp

#pragma once

#include "base.hpp"

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

    ~Derived() {
        TRACE_FUNCTION_CALL();
    }
};

// derived.cpp

#include "derived.hpp"

int main() {
    const Base &b = Derived {};
    TRACE_FUNCTION_CALL();
}

The output:

$ ./derived
Base::Base()
Derived::Derived()
int main()
Derived::~Derived()
Base::~Base()

It has to be pointed out that neither struct Base or Derived has a virtual desctructor. And when the lifetime of the temporary Derived object, which binds to a Base reference, ends, 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]

// exception-1.cpp

#include "base.hpp"

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

int main() {
    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:

$ ./exception-1
const Base& FunctionException1()
Base::Base()
Base::~Base()
int main()

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.

// exception-2.cpp

#include "derived.hpp"

struct DerivedWrapper {
    DerivedWrapper() : d {} {
        TRACE_FUNCTION_CALL();
    }

    ~DerivedWrapper() {
        TRACE_FUNCTION_CALL();
    }

    const Derived &d;
};

int main() {
    DerivedWrapper dw;
    TRACE_FUNCTION_CALL();
}

The result should be:

$ ./exception-2
Base::Base()
Derived::Derived()
DerivedWrapper::DerivedWrapper()
Derived::~Derived()
Base::~Base()
int main()
DerivedWrapper::~DerivedWrapper()

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. The warning message may looks like:

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]

// exception-3.cpp

#include "base.hpp"

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

int main() {
    const auto &b = FunctionException3({});
    TRACE_FUNCTION_CALL();
}

Here, the output:

$ ./exception-3
Base::Base()
const Base& FunctionException3(const Base&)
Base::~Base()
int main()

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:

// base-wrapper.hpp

#pragma once

#include "base.hpp"

struct BaseWrapper {
    const Base &b;
};

// exception-4.cpp

#include "base-wrapper.hpp"

// clang-format off
int main() {
    auto *w = new BaseWrapper { {} };
    TRACE_FUNCTION_CALL();
    delete w;
}
// clang-format on

And, the output on my machine:

$ ./exception-4
Base::Base()
Base::~Base()
int main()

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.

// special-case.cpp

#include "base-wrapper.hpp"

// clang-format off
int main() {
    BaseWrapper w { {} };
    TRACE_FUNCTION_CALL();
}
// clang-format on

The result shows:

$ ./special-case
Base::Base()
int main()
Base::~Base()

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” by Herb Sutter.
  2. Reference initialization
  3. The cost of std::initializer_list by Andrzej Krzemieński.
  4. Stack Overflow: Aggregate reference member and temporary lifetime