March 22, 2024·6 min read·engineeringcppcpp26safetyinitialization

Tokyo gives some uninitialized reads erroneous behavior

C++26 adopts a new category for incorrect programs whose behavior can still be constrained. P2795R5 preserves uninitialized reads as bugs while limiting some consequences previously left undefined.

Giving an uninitialized variable a predictable value can prevent one dangerous outcome while making the original mistake look intentional. The C++ committee's Tokyo decision addresses that tension by adding a category called erroneous behavior. Herb Sutter's 22 March meeting report identifies adoption of Thomas Köppe's P2795R5 for C++26. The change constrains some uninitialized reads while preserving their status as programming errors. It is a decision about the behavior of incorrect programs, with important limits on which incorrect programs it covers.

My reservations about C++ include the number of different language rules a large codebase can require a reader to know. A new behavior category adds another distinction, but this one has a concrete purpose. It lets the specification constrain a failure without telling programmers that the source expressed a legitimate default value.

Defaulting to zero would change the meaning of omission

P2795R5 motivates the change with a default-initialized automatic integer whose value is read before assignment. It compares its proposal with making omitted initialization mean zero. The latter would give existing syntax a new successful interpretation. A reviewer could no longer tell whether omission represented a deliberate choice of zero or a missed initialization.

The erroneous-behavior approach keeps the read conceptually wrong. The implementation supplies a value, and the language gives that error constrained behavior. Implementations may diagnose the mistake. The objective is to make an incorrect execution less dangerous without encouraging application logic to depend on that execution as if it were correct.

That is an important distinction for maintenance. An explicit initializer records the intended initial state. Omitting it can indicate that a later operation is responsible for producing the value. If the later operation is skipped, the program has failed to establish its own state. A safety measure should not silently rewrite that intent into a default the author never chose.

The proposal therefore concerns two different properties. The source can be incorrect according to the program's intended initialization discipline. An implementation can still have obligations about what happens when that mistake is encountered. Treating these as separate questions makes it possible to improve one without redefining the other.

The value is constrained without being a portable default

P2795R5 supplies implementation-defined initial values for the covered storage and describes the values as erroneous until replaced. It does not require that the value be zero. Its discussion considers the usefulness of different implementation choices for finding mistakes. It also clarifies that repeated reads of the same uninitialized value produce the same erroneous value.

Consider the following schematic error. The calls illustrate the behavior being changed, rather than a recommended way to produce a value.

void consume(int);

void missing_assignment()
{
    int count;
    consume(count);
}

The source still fails to initialize count meaningfully before use. Under the adopted approach for this covered case, the read has erroneous behavior. An implementation's chosen value is not a promise that count contains an application-appropriate number. Correct source should establish the value that consume is intended to receive.

The repeated-read rule matters because it constrains optimization and interpretation of the error. A program does not receive a new arbitrary integer on each read simply because initialization was omitted. At the same time, consistency does not make the value valid input to the next operation. An invalid size or index can cause another problem even when its numerical value is stable.

This is why the change needs a precise description. It reduces the consequences associated with one category of undefined behavior. It does not establish that every later operation using the resulting value is safe. Each operation retains its own requirements.

Pointers and invalid representations still need care

The paper explicitly discusses an uninitialized pointer whose erroneous value is later dereferenced. The dereference can still have undefined behavior. It also discusses representations that are not valid for their type, including a possible invalid boolean representation. Those caveats prevent a broad claim that every uninitialized read and everything following it now have a safe outcome.

The distinction is between obtaining a value under the changed initialization model and performing a valid operation with it. A pointer value cannot establish the existence of the pointed-to object merely by being predictable. A boolean representation cannot be treated as valid solely because the implementation filled the storage. The rules of the destination type remain relevant.

This is useful for understanding a security improvement at language level. A change can remove one unconstrained step in an execution while leaving other invalid steps possible. Its benefit should be assessed at the step it changes. Calling the whole program safe would require a much larger argument about all the operations that follow.

I prefer that narrower account because it helps explain what code still needs fixing. A diagnostic or a predictable failure can make an initialization mistake easier to locate. The application still needs to establish a valid value before using it as a size, a pointer or a state transition.

The opt-out restores an older obligation

The proposal includes [[indeterminate]] for supported automatic variable definitions and function parameters. This requests indeterminate storage instead of the new erroneous initialization behavior. Reading an opted-out uninitialized value can again cause undefined behavior. The attribute is a way to disable a safety mechanism where its cost is unacceptable.

The paper warns against interpreting that syntax as a modern spelling for deliberately postponing initialization. Those are different intentions. A program can correctly assign a variable later without requesting that an accidental earlier read regain undefined behavior. The opt-out should be understood in terms of the guarantee it removes.

Its compatibility properties explain the use of an attribute. Code correct with the attribute remains correct if it is removed or ignored. The attribute does not make a previously invalid read valid. Instead, it narrows what the implementation must provide when the programmer makes that error.

This creates an explicit review question for performance-sensitive code. The source can identify where it requests the old behavior, and the surrounding code must justify why all reads follow valid initialization. A project rule that mechanically annotates every variable with delayed initialization would conflate the implementation cost decision with the ordinary data-flow decision.

Automatic storage is only part of the model

The wording distinguishes automatic storage from dynamic storage. P2795R5 also discusses copying indeterminate data into an initialized object, and explains that the new initialization behavior does not redefine all uses of indeterminate values. The origin and representation of the value remain part of the analysis.

That boundary matters for wrappers around allocation and low-level copying. A local variable can be initialized and later receive data with different validity properties. The safety claim cannot be based only on the variable's declaration. Its subsequent operations determine which values reach the read being analyzed.

The paper also notes potential performance costs from initializing storage. Existing compiler hardening options demonstrate some related behavior, but a production initialization option does not necessarily diagnose the original bug. Constrained execution and error detection are separate implementation goals. A project evaluating the change needs to distinguish those goals in its expectations.

The revision updates std::bit_cast as well. Converting object representations needs to account for erroneous source bits alongside indeterminate ones. The paper treats unsigned character and std::byte destinations specially, with propagation rules that parallel the handling of indeterminate values. This is necessary because moving a representation and interpreting it as an ordinary value are different operations.

For low-level code, the implication is to follow the value through the conversion. A byte-oriented operation can transport information whose later interpretation still has constraints. Establishing that the copying step is permitted does not establish that any selected destination type has a valid value. The representation rules must remain part of the explanation.

Tokyo adopted this change while other work continues

Sutter's report separates the adopted uninitialized-read work from continuing contracts and reflection review. Those proposals advanced through discussions, with additional work still identified. A meeting report can describe substantial progress without every highlighted facility having entered the working draft.

For uninitialized reads, the concrete result is a new specification category with an initial application. A missing initialization remains a defect, while the implementation gains defined obligations for the cases covered by the change. The pointer and representation caveats keep that claim bounded. Maintainers can still require explicit valid state before use, with the revised rules limiting some consequences when that requirement is accidentally violated.