August 15, 2024·4 min read·engineeringcppconstexprcontainersallocation

Constexpr containers still need to release their temporary storage

P3372R0 proposes extending constant evaluation across containers and adaptors. The remaining allocation and hashing limits explain which compile-time algorithms that extension would enable.

A compile-time algorithm may need a set while it runs without needing that set to exist in the executable. That distinction is central to Hana Dusíková's P3372R0, dated 15 August. It proposes constexpr support across standard containers and adaptors, extending the set of ordinary library operations available during constant evaluation. It does not solve the separate problem of letting allocated compile-time storage survive into runtime.

C++ templates can provide useful abstractions without a runtime layer, but the cost of maintaining a second style of compile-time code is substantial. I favor making ordinary library algorithms available in constant evaluation when their behavior fits the model. The useful gain is reuse of an understandable algorithm, with its lifetime requirements still explicit.

The container can be an intermediate object

P3372R0 proposes marking the relevant container operations and iterator functionality constexpr. Its motivation is to reduce differences between runtime C++ and constant-evaluated C++. The intended extension includes linked and associative containers alongside adaptors. The paper identifies implementation work and unresolved details, rather than claiming that every operation already works in a released standard library.

An illustrative proposed use is to build a temporary set and return a count. The following example describes the proposal's intended facility, with no claim that current C++23 libraries accept it.

#include <cstddef>
#include <set>

constexpr std::size_t distinct_count()
{
    std::set<int> values{4, 1, 4, 2};
    return values.size();
}

static_assert(distinct_count() == 3);

The set is working storage for the calculation. Its destructor runs before the evaluation finishes, while the result is a simple count. The output does not need to preserve the set's allocation. This is a different requirement from declaring a persistent constexpr set whose allocated nodes must remain reachable later.

The difference is useful when choosing a representation. An algorithm might need dynamic organization while computing a result, then emit a fixed representation. That can keep the generation algorithm straightforward without requiring its working data structure to become the runtime artifact. The compile-time and runtime representations can have different purposes.

Transient allocation already has a history

The earlier P0784R7 describes transient constexpr allocations as allocations released before the containing evaluation completes. It records removal of a proposed non-transient allocation mechanism after the design was judged too brittle. That history helps explain the boundary P3372R0 retains.

The issue is larger than whether an allocator function has a constexpr declaration. Storage retained by a completed compile-time result needs rules for its later identity and destruction. A pointer embedded in that result would have to refer to storage with a meaningful lifetime in the generated program. Merely permitting allocation during evaluation cannot establish those properties.

For an application author, the immediate opportunity is therefore a computation whose result can leave its temporary allocations behind. A count, a boolean validation result or a fixed output object can serve that purpose. The exact representation needs to follow the algorithm's requirements, without assuming that the temporary container can simply be returned into a persistent constexpr variable.

Constant evaluation examines implementation details

Dusíková's implementation experiment found problems in container node construction and representation manipulation. The paper discusses allocating a node while constructing only a value subobject, as well as issues in hash calculations. Constant evaluation made these assumptions visible during implementation work. The author reports changes in a personal libc++ fork, which is evidence of experimentation rather than a released-library guarantee.

This is an interesting secondary benefit. Making an implementation available to constant evaluation requires its low-level operations to fit that execution model. A runtime implementation can have assumptions whose validity has not been exercised in that context. The work therefore needs more than adding a keyword to public declarations.

That does not mean every compile-time rejection proves a runtime defect. The two evaluation contexts have different permissions and implementation facilities. A useful diagnosis must distinguish a forbidden constant-evaluation operation from an operation that violates the ordinary language rules too. The paper's concrete findings provide subjects for that analysis.

I value this aspect because library abstraction should include the implementation discipline needed to support it. A declaration promising constexpr use is only useful if the full path through allocation, construction and destruction satisfies the relevant requirements. Container complexity can remain hidden from the user while implementers account for it precisely.

Hashing remains a specific obstacle

P3372R0 separately discusses pointer hashing and the absence of suitable constexpr support in other operations used by unordered containers. Its experiment extends pointer-to-integer handling, then notes that compile-time and runtime hashes can differ. This is explicitly unresolved design territory in the initial paper.

The distinction matters because a container's data structure is only one part of its operation. An unordered container also invokes a hash and an equality predicate. A program seeking constant evaluation needs those selected operations to support it as well. Making the container member constexpr cannot supply missing behavior in a user-provided callable.

Pointer identity adds another concern. A symbolic address during translation is not automatically the numerical address the program will use at runtime. An algorithm that relies on numerical hash values crossing that boundary needs a stronger promise than a container accepting pointer keys during one transient evaluation. The paper calls attention to that separation.

The paper also calls out node_handle::key(), which exposes mutable key access despite the const key representation inside a map node. It connects that problem to an existing core issue. This is another example of a specification boundary that a broad constexpr annotation cannot settle by itself. Node extraction has an ownership contract as well as a representation contract, and both need to remain valid when the operation is evaluated during translation.

The proposal's immediate value is a direction for library consistency. More ordinary data structures could be used as temporary working storage in compile-time algorithms. Its allocation limit keeps the output requirement distinct from that working storage, while the hashing discussion identifies operations that need further design. Those boundaries let a library author describe a concrete supported computation instead of assuming that every runtime container object can become a persistent compile-time value.