C++23 finishes with repairs to lifetime and constant evaluation
Issaquah completes technical work on C++23, including changes that make range loops and compile-time execution more consistent with ordinary code.
Changing direct member access into a getter should not silently invalidate a range loop. That is one of the practical problems addressed in the completed C++23 feature set. The temporary owning the range needs to survive the loop, and the expression used to obtain the range has historically made that lifetime surprisingly easy to misjudge.
Herb Sutter's February 13 Issaquah report says the committee completed C++23's technical work on the preceding Saturday. The final meeting resolved remaining national-body comments and added or removed no features. The next steps concern the approval ballot and editorial work, so technical completion should be distinguished from publication and universal implementation availability.
RAII remains C++'s strongest argument for me. Destruction gives resources a predictable endpoint, but that benefit depends on understanding exactly when the endpoint occurs. The range-loop lifetime correction is interesting because it removes a discrepancy in a basic control structure while also demonstrating why destruction timing is observable program behavior.
A getter can expose a temporary's contents
The range-loop issue is documented in P2644R1, led by Nicolai Josuttis. The paper lists cases involving getters returning references, nested containers, optionals, and spans. In each case, the range expression can refer into a temporary whose lifetime does not otherwise last through the loop.
An owning temporary and a reference into it are different objects for lifetime purposes. Keeping the reference used by the loop does not by itself guarantee that the owner remains alive. If the owner is destroyed before iteration, the loop's apparently convenient syntax hides a reference to storage that no longer belongs to a live object.
A getter returning a container reference makes the problem concrete. The following example illustrates the intended C++23 improvement.
#include <vector>
struct readings {
std::vector<int> values;
const std::vector<int>& get() const { return values; }
};
readings sample()
{
return {{2, 4, 6}};
}
int total()
{
int result = 0;
for (int value : sample().get()) {
result += value;
}
return result;
}
The getter returns a reference to the vector inside the temporary readings object. Here, the lifetime rule must preserve that owner while the loop uses its vector. The example depends on implementing the C++23 correction, not merely accepting range-loop syntax that already existed in earlier language modes.
The paper emphasizes how ordinary refactoring can expose this problem. Direct member access and a getter can appear to provide the same range while interacting differently with earlier lifetime-extension rules. The change makes a common abstraction step less likely to invalidate code that remains visually similar.
The rule extends particular temporaries
P2644R1 describes extending eligible temporaries created in the range initializer until the statement completes. Its wording excludes function parameter objects. That exclusion prevents the change from being interpreted as a general repair for every function that returns a dangling reference.
A function that accepts a container by value and returns a reference to that parameter still has a broken lifetime contract. The parameter is the function's own object. The caller cannot make that reference valid simply by placing the call in a range initializer.
This boundary is worth stating because a broad claim that temporaries now live through range loops is incomplete. The rule repairs temporary ownership in the initializer. It does not legitimize references returned from objects whose lifetimes have already ended inside a called function.
The paper also proposes updating the range-loop feature-test macro. That reflects the distinction between a compiler understanding the syntax and implementing the changed lifetime semantics. A project needs the latter if it relies on the new guarantee for an owning temporary reached through a reference-returning operation.
For me, this is a substantial usability correction. A basic loop should not require a programmer to know an exception involving a getter before it can be used safely. The explicit boundary around function parameters still leaves the underlying lifetime model recognizable.
The distinction also helps separate an owning return value from a view. A view can describe storage without owning it, so preserving the view alone cannot preserve a separately destroyed owner. The range initializer must keep the relevant owning temporary alive. That is the relationship demonstrated by the getter example, where the returned reference names a member of another object.
This changes the reasoning needed at the loop boundary. Under the corrected rule, the initializer can create the owner and obtain its contents while the loop retains the relevant lifetime. A helper still needs to return a valid reference to that owner, and an implementation still needs to apply the specified extension. The language feature addresses the particular destruction point that made the old expression unsafe.
That is a more useful guarantee than requiring every caller to recognize the same expression pattern and write a local workaround. A standard control structure can account for the temporary directly. Source using an earlier language mode may still need to name the owner separately, since the same textual loop does not establish the same lifetime rules in every mode.
Later destruction can change correct behavior too
Extending a temporary's lifetime moves its destructor call. That can affect more than memory retention. If the temporary owns a resource, the resource remains held until the new destruction point.
P2644R1 discusses lock-owning access helpers in its compatibility analysis. Keeping such a temporary alive through a loop can retain a lock across the loop body. Code that then attempts another acquisition can behave differently from code that released the helper at the end of the initializer.
This follows directly from RAII's mechanism. The destructor releases the resource, so moving the destructor changes the interval during which the resource is held. A lifetime extension that repairs a reference can therefore also extend a synchronization obligation.
The consequence is specific to the initializer's temporary objects. An ordinary range loop over an already live container has no newly created owner to retain in this way. The affected cases are the ones in which initialization creates an object with a meaningful destructor.
A useful review of this feature asks what each temporary owns. That question explains both the safety benefit and the compatibility effect. The issue is not simply whether an object contains data, but whether its lifetime controls a lock or another resource whose duration matters to the loop body.
Constant evaluation gains a dedicated branch
Another C++23 change highlighted in the completion report is if consteval. P1938R3 explains why an ordinary test of std::is_constant_evaluated() does not solve every interaction between constexpr and immediate functions.
A consteval function must be called under the rules for immediate invocation. A constexpr function can execute during constant evaluation or at runtime. Simply checking whether evaluation is constant does not, under the earlier rules, establish the special context needed for every call to an immediate function using the surrounding function's parameter.
The new statement supplies that context in its constant-evaluation branch. The paper's mechanism is more specific than a syntactic abbreviation for a Boolean test. It changes the context in which the selected branch can invoke immediate functions.
Both paths can compute the same numeric result. The example uses an immediate helper only during constant evaluation.
consteval int identity(int value)
{
return value;
}
constexpr int adjusted(int value)
{
if consteval {
return identity(value) + 1;
} else {
return value + 1;
}
}
static_assert(adjusted(4) == 5);
The call to identity uses the immediate-function context provided by the first branch. The runtime branch does not call that function. This permits a single interface to select implementations appropriate to the phase in which it is being evaluated.
Similar spellings had encouraged the wrong test
P1938R3 also addresses the tempting spelling if constexpr (std::is_constant_evaluated()). The condition of if constexpr is itself constant-evaluated, so that combination does not ask the runtime-versus-compile-time question its author may intend.
The dedicated syntax avoids that particular confusion. It requires braces and has no parenthesized Boolean condition. The paper explains that the visible distinction helps communicate entry into a different evaluation context.
This does not remove the library function. The proposal retains uses where a function-valued query remains appropriate, including conditions combined with other tests. C++23 therefore supplies a dedicated control-flow form while preserving the existing query for the situations that need it.
The lifetime and evaluation changes share a concern with how familiar constructs compose. A getter inside a range expression should not defeat the owner's lifetime. A phase-selecting branch should provide the context needed by an immediate function. Both repairs reduce the number of special rules that library authors must reproduce around ordinary-looking code.
C++23's technical completion fixes the feature set while implementations continue their own work. The practical adoption unit remains the individual facility and its semantics. For these two changes, that means temporary lifetime through the range statement and an immediate-function context inside the constant-evaluation branch.