October 7, 2018·5 min read·engineeringcppconstexprstringsallocation

A constexpr string needs more than constexpr member functions

P0980R0 proposes constant-evaluation support for basic_string. Character traits, storage lifetime and existing library instantiations explain why adding constexpr to the declarations is only part of the work.

Making std::string usable during constant evaluation requires agreement between the library's interface and the language's storage model. Louis Dionne's October 7 proposal, P0980R0, supplies wording for the string interface and identifies implementation questions found while examining libc++. The surrounding work on allocation and destruction makes this more substantial than allowing a compile-time call to size.

The proposal's stated motivation includes reflection and other compile-time programming. A program that computes names or text needs a representation it can manipulate. If its compile-time representation differs entirely from the runtime library's string, the programmer has to maintain another set of operations and constraints.

I value the possibility of reusing ordinary C++ abstractions during constant evaluation. Templates already make compilation a substantial part of the programming model. Giving that work a familiar owning string could simplify code, provided the ownership and destruction rules are specified clearly enough to explain where its storage goes.

The proposed interface includes mutation and destruction

P0980R0 adds constexpr across constructors, assignment and the destructor, together with iterators and capacity operations. It also covers element access and modifiers such as append. The intended string is consequently an object that can be constructed, changed and destroyed during evaluation, rather than just a read-only view over existing characters.

The nonmember concatenation and comparison operators receive corresponding treatment. String literal suffix functions are also included. These additions aim to let ordinary combinations of string operations participate in a larger constant computation. A single non-constexpr operation in the required path would otherwise interrupt that computation.

The proposed synopsis does not indiscriminately mark every adjacent string-related function. Stream insertion and extraction remain ordinary declarations, as do the displayed numeric conversion functions. That boundary helps define the scope. Owning and manipulating characters in constant evaluation is different from making the complete stream and numeric-conversion machinery available there.

A prospective computation can illustrate the desired use without claiming that current C++17 accepts it:

// Illustrative use of the proposed constexpr string facilities.
#include <string>

constexpr bool joined_name_matches()
{
    std::string name = "cache";
    name += "_entry";
    return name == "cache_entry";
}

static_assert(joined_name_matches());

The function returns a Boolean, so the temporary string does not need to survive the completed constant evaluation. The example depends on the proposal and its supporting language work. It is not a portable C++17 example or a promise about an existing library implementation.

Allocation lifetime is the central language dependency

The supporting P0784R3 proposal, dated May 4, distinguishes transient allocations from non-transient ones. A transient allocation is created during constant evaluation and deallocated before that evaluation ends. That model supports an owning container used as a temporary workspace while computing a simpler result.

The joined_name_matches example has that shape. Its string exists while building and comparing text, and its destructor runs before the function returns its Boolean result. The compiler still needs rules permitting construction, storage use and deallocation during evaluation, but the result does not retain a pointer to that temporary storage.

P0784R3 also proposes a route for allocations that remain after initialization of a constexpr variable. Its conditions involve a nontrivial constexpr destructor whose evaluation would deallocate all the retained allocations. Under that proposed model, the allocation can be promoted to static storage. This is a stronger facility than temporary storage used and released within one evaluation.

That distinction prevents two different goals from being treated as equivalent. Using a string to compute a number or Boolean requires temporary ownership. Preserving a string value with allocated storage for later program use requires an account of that storage's lifetime beyond the evaluation. The proposal explicitly extends earlier work to address the latter case.

The destructor consequently has a role beyond ordinary cleanup. In the non-transient proposal, evaluating its effect establishes whether the retained allocations belong to the object in the required way. Storage promotion depends on that analysis. The paper also restricts deallocating such promoted storage by another route, so the proposed persistence mechanism cannot be interpreted as an unrestricted runtime heap allocation that happened to originate during compilation.

The implementation must expose the right operations

Dionne's libc++ survey identifies character traits as a prerequisite. At least the char specialization needs the required constexpr behavior. The string interface delegates operations to its traits, so annotating the outer member cannot make an unavailable inner operation usable in a constant expression.

The survey also considers the short-string representation. The authors expected short-string optimization to be problematic, but report that libc++ implements it with unions and does not need reinterpret casts for the examined representation. That is a finding about the surveyed implementation, not a guarantee that every string implementation has the same internal structure.

Another issue is libc++'s explicit instantiation of basic_string<char> in a shared library. The paper says the interaction needs investigation. It expects the arrangement to work if the member definitions remain in the headers, but does not present that expectation as a completed compatibility result.

These observations show the two directions of the implementation task. The library needs definitions that constant evaluation can use. It also has existing runtime and binary arrangements that cannot be ignored merely because the declarations acquire constexpr. A usable proposal needs to connect those arrangements without relying on undocumented representation assumptions.

I find the implementation survey valuable because it identifies those dependencies directly. Compile-time usability is a property of the complete operation path. Traits and the owning object's representation remain relevant even when the application sees one familiar string interface.

Container work supplies the broader motivation

P0784R3 describes a compile-time JSON experiment that had to use fixed-size structures and parse twice, first to determine sizes and then to populate the representation. Variable-sized containers would address that kind of duplication by allowing intermediate storage to follow the computation's needs.

The proposal states that its non-transient extension had not yet been implemented, while reporting preliminary implementer discussions without identified prohibitive blockers. That limits the conclusion available from these papers. P0980R0 provides detailed string wording and identifies specific implementation concerns. Supporting container work proposes the allocation and destruction model needed to make those operations useful during constant evaluation.