· 6 min read ·

The Question Constexpr Always Knew the Answer To: C++26 and Union Lifetimes

Source: isocpp

The relationship between unions and constexpr has always been tense. Unions are one of C++‘s most expressive tools for building space-efficient sum types, but the rules around which member is “active” at any given time create friction that compile-time evaluation makes far more visible. C++26 addresses this directly with std::is_within_lifetime, a function proposed in P2641R4 that lets you query the compiler’s own lifetime tracking during constant evaluation. Sandor Dargo’s writeup on isocpp.org covers the basics well; what is worth dwelling on is why this feature had to exist at all, and what it reveals about how constexpr evaluation works as a model.

The Active Member Rule and Why Constexpr Enforces It Harder

C++ unions allow only one member to be “active” at a time. The last member you wrote to is the active one. Reading from an inactive member is undefined behavior:

union U {
    int i;
    float f;
};

U u;
u.i = 42;
float x = u.f; // undefined behavior: f is not the active member

At runtime, this UB is mostly silent. The bits are present in memory, compilers may exploit the aliasing assumption for optimization, and programs frequently get away with type-punning through unions on specific implementations. The behavior is not defined, but the consequences are often invisible.

Constexpr evaluation is different. The abstract machine that runs during constant evaluation tracks object lifetimes precisely, and it enforces the active member rule as a hard compile-time error:

constexpr float bad() {
    U u;
    u.i = 42;
    return u.f; // compile error: reading inactive union member
}

This is the compiler doing the right thing. But it creates an immediate problem: how do you write code that conditionally accesses a union member depending on whether it happens to be active? At runtime you can track this with a separate flag. At constexpr time, the evaluator already knows, but until C++26 there was no way to ask it.

The Optional Problem

The canonical motivation for std::is_within_lifetime is std::optional. A typical implementation uses a union between T and an empty sentinel, plus a boolean to track which is active:

template<typename T>
class optional {
    union {
        T val_;
        unsigned char empty_;
    };
    bool has_val_ = false;

public:
    constexpr bool has_value() const noexcept {
        return has_val_;
    }

    constexpr const T& value() const {
        if (!has_val_) throw std::bad_optional_access{};
        return val_;
    }
};

The boolean has_val_ exists entirely because there was no other way to check. During constant evaluation, the compiler tracks exactly which union member is active — that information is already present inside the evaluator, maintained as part of the abstract machine state. The boolean is a redundant shadow of knowledge the compiler already holds.

The redundancy is not just philosophically unsatisfying. It also means the implementation has two sources of truth about the same fact. If they disagree, you have a bug. Writing correct constexpr constructors, destructors, and assignment operators for optional requires carefully keeping the bool in sync with the union’s actual state, and there are subtle cases, particularly around exception safety and move operations, where getting this right is non-trivial.

The API

std::is_within_lifetime lives in <type_traits> and has this signature:

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

The consteval specifier is the most important detail. The function can only be called during constant evaluation. This is not a limitation — it is a precise statement of what the function is: an introspection into the constexpr evaluator’s lifetime tracking state. At runtime, that state does not exist in any accessible form. There is no runtime analog because the question only makes sense in the context of a fully-tracked abstract machine.

Given a pointer to a union member, calling is_within_lifetime tells you whether that member is the active one:

union Storage {
    int value;
    unsigned char empty;
};

consteval bool is_value_active(const Storage& s) {
    return std::is_within_lifetime(&s.value);
}

constexpr Storage with_value = [] {
    Storage s;
    s.value = 42;
    return s;
}();

constexpr Storage without_value = [] {
    Storage s;
    s.empty = 0;
    return s;
}();

static_assert(is_value_active(with_value));    // passes
static_assert(!is_value_active(without_value)); // passes

With this, you can implement has_value() for a union-based type without a separate tracking boolean, at least in constexpr contexts. Combine it with C++23’s if consteval and the two evaluation paths separate cleanly:

template<typename T>
class optional {
    union {
        T val_;
        char empty_ = '\0';
    };
    bool has_val_ = false; // still needed for runtime

public:
    constexpr bool has_value() const noexcept {
        if consteval {
            return std::is_within_lifetime(&val_);
        } else {
            return has_val_;
        }
    }
};

The runtime path uses the boolean as before. The constexpr path uses the evaluator’s own knowledge directly. The two can never disagree because they operate on separate evaluation regimes.

Not Just Unions

The function is not restricted to union members. It applies to any pointer to any object, and the question it answers is general: is this object currently within its lifetime during this evaluation? That covers base class subobjects, members of objects that have been destroyed, pointers to stack objects after their scope has ended, and similar lifetime edge cases that arise in complex constexpr code.

In practice, the union case is overwhelmingly the most common motivation and the most useful application. But the broader formulation means the feature has a clean, unified semantic rather than being a special-purpose union introspection hack. That matters for the long-term consistency of the language.

What This Reflects About Constexpr as a Model

There is a useful analogy to GCC’s __builtin_constant_p, which tests whether the compiler knows a value to be a compile-time constant. Both are introspection functions: they expose some of the compiler’s internal reasoning to user code. The difference is that __builtin_constant_p is an extension with implementation-defined semantics, while is_within_lifetime is a standard feature with a precisely specified meaning.

This is part of a broader pattern in modern C++ of formalizing what was previously compiler-specific knowledge. Reflection in C++26 does the same thing at a larger scale: it takes the type metadata that compilers have always maintained internally and makes it accessible through a standardized API. is_within_lifetime is a much smaller and more focused version of the same idea.

The consteval restriction also reflects a wider trend in C++26 toward being explicit about which operations only make sense at compile time. Rather than defining a constexpr function with runtime behavior that is vacuously true or implementation-defined, the standard acknowledges that this question has no meaningful runtime analog and restricts the function accordingly. That honesty is preferable to a function that silently becomes a no-op at runtime.

Implications for Library Implementors

Anyone writing std::optional, std::variant, std::expected, or a custom sum type using a union will find this immediately useful. The pattern of using if consteval to dispatch between is_within_lifetime and a runtime boolean gives you a constexpr path that is tightly coupled to the evaluator’s actual state, and a runtime path that uses whatever mechanism your implementation requires.

For std::variant specifically, the gain is significant. Variant has to track which alternative is active among potentially many types, typically with an index stored alongside the union. At constexpr time, checking each alternative’s pointer with is_within_lifetime gives you a way to verify or reconstruct that index from ground truth rather than trusting a cached value. Implementations that were previously careful-but-redundant can be simplified on the constexpr path.

C++26 has several features that address specific, long-standing friction points in constexpr programming. std::is_within_lifetime is compact in scope but precise in what it fixes. The constexpr evaluator has always known which union member is active; C++26 finally gives you a way to ask.

Was this interesting?