March 4, 2025·4 min read·engineeringcppclanglifetimeinitialization

Clang 20 fixes lifetime extension through default member initializers

A temporary bound through aggregate initialization needs the correct destruction point. Clang 20 implements that rule and completes related C++23 range-for lifetime support.

RAII depends on knowing when destruction occurs. That becomes less obvious when an aggregate contains a reference initialized from a temporary, especially when the initializer lives in the member declaration instead of the aggregate initializer list. Clang 20 implements a long-standing resolution for that case, bringing its behavior into line with the lifetime rule described by CWG1815.

The LLVM 20.1.0 release appeared on March 4. Its Clang notes identify support for extending the lifetime of a temporary created through a default member initializer during aggregate initialization. The same release completes a related part of the C++23 range-based for lifetime rule.

I regard RAII as C++'s strongest practical argument. Its usefulness depends on more than destructors running at some eventual point. References and owning objects need a precise relationship between construction and destruction. These changes concern that relationship in code whose syntax can look almost identical while selecting different initialization rules.

Aggregate initialization can use a member's initializer

Core issue 1815 began with a question submitted by Dinka Ranns in November 2013. Adding default member initializers to aggregates exposed a lifetime question for reference members. The issue compares direct default initialization with aggregate initialization using either an explicit temporary or the member's default initializer.

The essential aggregate case can be written as follows. The member's initializer constructs the temporary, and the empty aggregate initializer uses it.

struct Value {};

struct Holder {
    Value&& value { Value{} };
};

Holder holder{};

The issue asks whether this aggregate form should extend the lifetime as an explicit aggregate initializer would. Its recorded direction treats the aggregate cases alike. Clang 20's release note names implementation of that resolution, so the important event is compiler support for an older language correction.

This is different from claiming that a reference member always extends the lifetime of whatever expression initializes it. The issue explicitly distinguishes the default-constructor route. Which initialization rule is selected remains material. Looking only at the type of the member omits the operation that establishes the temporary's lifetime.

The history illustrates a recurring maintenance issue in C++. A language rule can have an old resolution while implementations acquire complete support later. Code that uses the rule depends on both the intended semantics and the compiler's implementation of that path. A new compiler release can consequently repair behavior for a construct that is not a new language feature.

Range-based for has another temporary boundary

Clang 20 also extends temporaries in member default initialization for P2718R0. Its release notes say this completes support for the C++23 lifetime extension in range-based for loops. The P2718R0 wording, dated November 11, 2022, describes which temporaries persist through the loop.

The wording covers a temporary created in the for-range initializer that would otherwise be destroyed at the end of that full expression. It extends the object for the lifetime of the reference initialized by that initializer. Function parameter objects are excluded. That qualification prevents the rule from repairing every function that returns a reference to an object already destroyed inside the call.

The paper illustrates the distinction with two helpers. One accepts a reference and returns it. Another accepts an object by value and returns a reference to that parameter. Extending the caller's temporary can support the first shape. It cannot make the second helper's destroyed parameter remain live after the function returns.

This is a useful boundary to retain in an explanation of the compiler change. A broader claim that range-based for now makes every chained expression safe would conceal the exact ownership failure in the by-value helper. The lifetime extension is tied to specified temporaries, not to an arbitrary reference that happens to appear in the range expression.

Later destruction can change valid programs

P2718 also records a compatibility consequence. A temporary whose lifetime now extends through the loop has a destructor that runs later. That can affect resources with visible release behavior, even where the earlier code did not dereference a dangling object.

The paper gives a lock guard in the range initializer as an example. When the guard survives through the loop, attempting to lock the same non-recursive mutex again inside the loop can deadlock. The change intended to preserve temporary objects also preserves their resource ownership for longer.

That is an implication of RAII itself. Extending an object's lifetime extends the interval before its destructor releases the resource. A lifetime correction can therefore fix a dangling access in one expression while exposing an unintended lock duration in another. Both outcomes follow the destruction point, with no contradiction in the rules.

I find that more useful than describing lifetime extension as an unconditional safety improvement. The specific guarantee is continued existence of the selected temporary. If that temporary owns a lock or another scoped resource, the programmer also inherits the longer ownership interval.

P2718 also specifies reverse construction order when extended temporaries reach the same lifetime endpoint. That keeps destruction order part of the rule rather than leaving it to an intuition about the loop's closing brace. Multiple temporaries can own related resources, so preserving their lifetimes is only one part of the behavior. The ordering in which their destructors then execute determines which resources remain available during the other destructors.

Implementation status needs the particular rule

The release includes other C++ work, including C++26 placement new in constant evaluation and additional builtins. Those additions have their own proposal histories. They do not explain the aggregate-lifetime correction merely by sharing a release number with it.

For code depending on temporary lifetime, the useful evidence is the named rule and the affected initialization path. CWG1815 concerns aggregate initialization through a default member initializer. P2718 concerns selected temporaries in a range-for initializer, with parameter objects excluded. Clang 20 brings those paths into its documented implementation support, and their different boundaries still determine which object remains alive until which exit.