· 6 min read ·

The Compile-Time Union Problem That C++26 Finally Names

Source: isocpp

When Sandor Dargo wrote about std::is_within_lifetime, he described the same double-take most of us have: the name sounds like a generic lifetime-checking utility, something you’d reach for when debugging dangling pointers. Then you read the subtitle, “Checking if a union alternative is active,” and the feature feels oddly specific. But the specificity is the point. This is a feature that had to be precisely scoped because the underlying problem is precise, and the solution required a new kind of compile-time introspection that simply did not exist in the language before.

What the Function Actually Is

The signature is straightforward:

template <typename T>
consteval bool std::is_within_lifetime(const T* p) noexcept;

This lives in <type_traits> and is part of P2641R4, authored by Barry Revzin and Daveed Vandevoorde. The function takes a pointer and returns whether the object it points to is currently within its lifetime, at the moment the call is evaluated. The critical word in that sentence is consteval, not constexpr. This function can only run at compile time. You cannot call it at runtime. That restriction is not a limitation; it is the entire design.

At runtime, pointer values are memory addresses. There is no metadata attached to them that tracks whether the pointed-to object is “alive” in any formal sense. The runtime memory model simply does not carry that information. But the compile-time evaluator is different. When constexpr and consteval functions execute, the compiler tracks object lifetimes in precise detail, because it has to: the standard requires constant evaluation to be well-defined, and the only way to enforce that is to maintain an explicit model of what exists and when.

std::is_within_lifetime exposes a window into that internal model.

The Union Problem It Was Designed to Solve

Unions in C++ have a strict rule: only one member is active at a time, and reading from an inactive member is undefined behavior. At runtime, compilers are relatively lenient about enforcing this (especially for trivial types where type-punning is common), but at compile time the evaluator is strict. If you write a constexpr function that reads from the wrong union member, the compiler will reject it.

This creates a real problem when you want to implement something like std::optional using a union internally, which is the canonical implementation strategy:

template <typename T>
struct Optional {
    union Storage {
        T value;
        char dummy;
        constexpr Storage() : dummy{} {}
    } storage;
    bool engaged;

    constexpr bool has_value() const { return engaged; }
    constexpr T& operator*() { return storage.value; }
};

This works at runtime, but it has a problem in constant evaluation: if you want to implement has_value by inspecting the union directly rather than maintaining a separate boolean flag, you have no portable way to do it. You cannot ask the union “which member is currently active.” You can only know by tracking that state yourself with auxiliary storage.

Before C++26, standard library implementors worked around this with non-standard compiler intrinsics. GCC and Clang both have internal machinery to track union active members during constant evaluation, but it was never exposed to user code through any standard interface.

std::is_within_lifetime standardizes that introspection:

template <typename T>
struct Optional {
    union Storage {
        T value;
        char dummy;
        constexpr Storage() : dummy{} {}
    } storage;

    constexpr bool has_value() const {
        return std::is_within_lifetime(&storage.value);
    }
};

At compile time, calling std::is_within_lifetime(&storage.value) returns true if and only if value is the currently active union member. If dummy is active instead, it returns false. This is not a runtime check; it is querying the compile-time evaluator’s own bookkeeping.

Why consteval and Not constexpr

The choice to make this consteval rather than constexpr is deliberate and worth examining. A constexpr function can run at both compile time and runtime. A consteval function is guaranteed to run only at compile time; any call that cannot be evaluated at compile time is a compile error.

For std::is_within_lifetime, there is genuinely no runtime semantics to provide. The information it accesses does not exist at runtime. Making it consteval is not a restriction; it is an honest description of what the function can do. Attempting to give it a runtime fallback would mean fabricating an answer, and the wrong answer here leads to undefined behavior.

This follows the same reasoning behind std::is_constant_evaluated() from C++20, which also introspects the evaluation context in a way that would be meaningless at runtime. The difference is that std::is_constant_evaluated() returns a bool based on where it is called, while std::is_within_lifetime queries the lifetime state of a specific object.

The Standard Library’s Self-Hosting Problem

There is a broader principle at work here. A recurring goal in recent C++ standards has been making the standard library implementable using only standard language features. This sounds obvious, but it was historically false in practice. Implementations of std::optional, std::variant, std::string, and others relied on compiler-specific extensions to get full constexpr support.

std::variant is a particularly relevant case. Its std::get<N> and std::visit operations need to know which alternative is currently active. The internal discriminant is tracked by an index, but in a deeply constexpr context, checking whether a specific alternative’s storage is within its lifetime is exactly what std::is_within_lifetime enables. Without it, implementations had to rely on __builtin_is_within_lifetime or equivalent internal builtins, then guard those with #ifdef for portability.

C++23 made significant progress on compile-time standard library support, allowing std::vector and std::string in constant expressions. C++26 continues that trajectory. std::is_within_lifetime is a relatively small piece of that work, but it closes a gap that was real and that affected the implementability of core vocabulary types.

Comparison with Other Languages

Other languages handle the “which variant is active” question differently, and the comparison is instructive.

Rust’s enums are tagged unions. The tag is always present, always checked, and there is no concept of reading the “wrong” variant. The compiler tracks this through the type system at every level, not just during compilation of constant expressions. You cannot write Rust code that accidentally reads an inactive variant; the pattern-matching syntax forces you to handle all cases. There is no need for a function like is_within_lifetime because the invariant is structural rather than tracked separately.

Ada’s discriminated record types are similar in spirit: the discriminant is part of the type, and variant field access checks the discriminant automatically. The safety is baked into the type system.

C++ takes a different approach. Unions are unchecked by default for efficiency and compatibility reasons, and the tracking that exists in constant evaluation is an artifact of the evaluator’s need for correctness rather than a general language feature. std::is_within_lifetime exposes that evaluator state without trying to retrofit a tagged-union type system onto the language. It is a pragmatic solution rather than an architectural one.

A Note on What This Does Not Do

It is worth being clear about scope. std::is_within_lifetime does not:

  • Check for dangling pointers at compile time in general.
  • Detect use-after-free or use-after-scope bugs.
  • Provide runtime lifetime checking of any kind.
  • Replace tools like AddressSanitizer or Valgrind.

The name might suggest a broader utility, but the function is specifically about querying the compile-time evaluator’s union-member-activity model. Other uses are theoretically possible (any object whose lifetime has ended would return false), but the motivating case, and the case the standard text is designed around, is union active-member detection.

What Changes in Practice

For most C++ developers, std::is_within_lifetime will never appear directly in their code. Its primary consumers are standard library implementors and authors of deeply generic vocabulary types.

What changes in practice is that implementations of std::optional, std::variant, and similar types can be written using only standard C++26 without reaching for compiler intrinsics. That makes them more portable, more auditable, and easier to reason about. It also means that user-defined types modeled on the same patterns, custom optional types, discriminated unions, compile-time state machines, can be implemented with the same technique.

The feature is narrow in the sense that most code will never call it directly. It is broad in the sense that the standard library you use every day may depend on it for correctness in constant evaluation contexts.

That asymmetry is common in the lower layers of any language standard. The primitives that most people never touch are often the ones that make everything else work.

Was this interesting?