p.enthalabs

Things C++26 define_static_array can’t do

Things C++26 `define_static_array` can't do – Arthur O'Dwyer – Stuff mostly about C++

![Image 1](https://quuxplusone.github.io/blog/)

Arthur O’Dwyer

Stuff mostly about C++

RecentDateTagTrainingAbout

Things C++26 ```plaintext define_static_array ``` can’t do

We’ve seen previously that it’s not possible to create a ```plaintext constexpr ``` global variable of container type, when that container holds a pointer to a heap allocation. It’s fine to create a global constexpr ```plaintext std::array ``` , or even a ```plaintext std::string ``` that uses only its SSO buffer; but you can’t create a global constexpr ```plaintext std::vector ``` or ```plaintext std::list ``` (unless it’s empty) because it would have to hold a pointer to a heap allocation.

Think of constexpr evaluation as taking place “in the compiler’s imagination.” Since C++20 it’s fine to use ```plaintext new ``` and ```plaintext delete ```

at constexpr time; but there’s a firewall between constexpr evaluation and real, material runtime existence. You can’t, at runtime, get a pointer to a heap allocation that was made only “in the compiler’s imagination,” any more than you can get a pointer to a local variable of a stack frame that was made only “in the compiler’s imagination.” So none of these snippets will compile:

``` constexpr int *f() { int i = 42; return &i; } constinit int *p = f(); // error

constexpr int *f() { return new int(42); } constinit int *p = f(); // error

constexpr std::vector<int> f() { return {1,2,3}; } constinit std::vector<int> p = f(); // error ```

But if you can compute a ```plaintext std::vector<int> ``` at constexpr time, then you can persist its contents into a global constexpr ```plaintext std::array ``` of the appropriate size. The appropriate size is just the ```plaintext .size() ``` of the vector you computed, of course. So we have what’s become known as the “constexpr two-step” (Godbolt):

``` constexpr std::vector<int> f() { return {1,2,3}; }

constinit auto a = []() { std::array<int, f().size()> a; std::ranges::copy(f(), a.begin()); return a; }(); ```

Thanks to Barry Revzin’s P3491 (June 2025) and Jason Turner’s “Understanding the Constexpr 2-Step” (C++ On Sea 2024) for the term “constexpr two-step.” Jason’s talk deals with a specific formula in which instead of _repeating_ — and repeatedly evaluating — ```plaintext f() ``` in the body of the lambda, we factor it out into a template argument (Godbolt):

``` constexpr std::vector<int> f() { return {1,2,3}; }

template<auto B> consteval auto to_array() { // MAGIC NUMBER WARNING! constexpr auto v = B() | std::ranges::to<std::inplace_vector<int, 999>>(); std::array<int, v.size()> a; std::ranges::copy(v, a.begin()); return a; }

constinit auto a = to_array<[]() { return f(); }>(); ```

C++26 will introduce a new and improved tool for this kind of compile-time array generation. It’s spelled ```plaintext std::define_static_array ``` . In C++26 you can just write this (Godbolt):

``` constexpr std::vector<int> f() { return {1,2,3}; } constinit std::span<const int> sp = std::define_static_array(f()); ```

This call to ```plaintext define_static_array ``` returns a ```plaintext span ```

over a static-storage constant array of three ints. Basically this is asking the compiler to take the data it’s come up with “in its imagination” and write down a copy of it in the object file. This is much cleaner and more compile-time-efficient than the “two-step”!

Unfortunately, if I understand it correctly, C++26 ```plaintext define_static_array ``` does not (yet?) support several things that you _can_ do using the “two-step.” Here are a few such things.

1. Non-structural types

```plaintext std::define_static_array ``` is defined in terms of ```plaintext std::meta::reflect_constant(e) ``` , which C++26 defines as ```plaintext std::meta::template_arguments_of(^^TCls<e>)[0] ``` for some invented template ```plaintext TCls ``` . That is, ```plaintext reflect_constant ``` (and thus ```plaintext define_static_array ``` ) is defined only for structural types. ```plaintext int ``` is a structural type, and thus we can write the code above. But we cannot write

``` using OInt = std::optional<int>; constexpr std::vector<OInt> f() { return {1,2,3}; } std::span<const OInt> sp = std::define_static_array(f()); ```

because ```plaintext optional<int> ``` is not a structural type. Nor are ```plaintext string ``` , ```plaintext string_view ``` , ```plaintext span ``` itself… There are many types that can’t be materialized using ```plaintext define_static_array ``` , even though they work fine with the “constexpr two-step” (Godbolt).

2. Pointers to string literals

Because ```plaintext reflect_constant ``` is defined in terms of ```plaintext TCls<e> ``` , not only must the _type_ of ```plaintext e ``` be structural, but each particular _value_ ```plaintext e ``` in the array must be suitable for use as a template argument. ```plaintext const char* ``` is a structural type, but if that pointer points to a string literal, then it’s not suitable for use as a template argument. So we can use ```plaintext define_static_array ``` to make an array of null pointers:

``` constexpr std::vector<const char*> f() { return {nullptr, nullptr, nullptr}; } std::span<const char *const> sp = std::define_static_array(f()); ```

but it cannot make an array of pointers to literals:

``` constexpr std::vector<const char*> f() { return {"a", "b", "c"}; } std::span<const char *const> sp = std::define_static_array(f()); ```

On the other hand, the “constexpr two-step” has no problem with string literals (Godbolt).

3. Move-only types

In order to create a template parameter object representing ```plaintext e ``` , we must make a copy of ```plaintext e ``` ([[temp.arg.nontype]/4](https://eel.is/c++draft/temp.arg.nontype#4)). Therefore NTTP types must be copyable. You can (with care) use the two-step to create a static array of move-only type:

``` constexpr auto a = []() { std::array<MoveOnly, f().size()> a; std::ranges::copy(f() | std::views::as_rvalue, a.begin()); return a; }(); ```

but you cannot do the same with ```plaintext define_static_array ``` . (Godbolt.)

The above snippet, like all my other examples of the “two-step,” never actually uses move-construction; it uses default construction followed by assignment. This is unsatisfying, and prevents the two-step from creating e.g. an array of ```plaintext reference_wrapper ``` . ```plaintext define_static_array ``` , on the other hand, does not use default-construction (Godbolt). Can we rework the two-step to eliminate the default-constructibility requirement? I imagine we can, but at the moment I don’t see how.

4. Make the array mutable

```plaintext define_static_array ``` allocates its array in rodata and gives you a ```plaintext span<const T> ``` over it. This allows the compiler to do cool things, like point multiple invocations of ```plaintext define_static_array ``` at the same backing array (Godbolt). In fact, the compiler is actually _required_ to do that, because ```plaintext reflect_constant ```

is defined in terms of a template parameter object which for all intents and purposes behaves like an inline variable: there is guaranteed to be only one template parameter object with a given type and value in the whole program (Godbolt).

Treating template parameter objects as inline variables means the compiler _must_ combine such objects when they have the same type and value (optimization! hooray!) but sadly also _forbids_ an otherwise sufficiently smart compiler from combining such objects when their types are merely similar. Godbolt:

``` template<auto V> auto tpo() { return std::span(V); } template<auto V> auto tpo2() { return std::span(V); }

const void *p1 = tpo<std::array<signed char,3>{1,2,3}>().data(); const void *p2 = tpo2<std::array<signed char,3>{1,2,3}>().data(); const void *p3 = tpo<std::array<unsigned char,3>{1,2,3}>().data(); const void *p4 = tpo<std::array<char,3>{1,2,3}>().data(); ```

All four of these pointers point to arrays of the three bytes ```plaintext 01 02 03 ``` . ```plaintext p1 ``` and ```plaintext p2 ``` are required to point to the same byte; ```plaintext p3 ``` and ```plaintext p4 ``` , since they point to ```plaintext std::array ``` objects of different types, are required to point to different arrays. The compiler isn’t allowed to coalesce ```plaintext p3 ``` and ```plaintext p4 ``` , the way it’s allowed to coalesce the backing arrays of differently typed ```plaintext initializer_list ``` s (Godbolt).

But (hooray! and thanks to Tim Song for correcting me on this!) there is a special case specifically for the “template parameter objects of array type” created by ```plaintext reflect_constant_array ``` and ```plaintext define_static_array ``` . _These_ objects _are_ permitted ([[intro.object]/9.3](https://eel.is/c++draft/intro.object#def:object,potentially_non-unique)) to overlap or be coalesced, just like ```plaintext initializer_list ```

s and string literals. Clang trunk isn’t smart enough to coalesce potentially non-unique objects; therefore the Clang reference implementation of C++26 Reflection doesn’t coalesce these array objects either; but it’s not the paper standard’s fault. Godbolt:

``` const void *p1 = std::define_static_array(std::vector<signed char>{1,2,3}).data(); const void *p2 = std::define_static_array(std::list<signed char>{1,2,3}).data(); const void *p3 = std::define_static_array(std::vector<unsigned char>{1,2,3}).data(); const void *p4 = std::define_static_array(std::vector<char>{1,2,3}).data(); ```

All four of these pointers point to arrays of the three bytes ```plaintext 01 02 03 ``` . ```plaintext p1 ``` and ```plaintext p2 ``` are required to point to the same byte; ```plaintext p3 ``` and ```plaintext p4 ``` are permitted, but not required, to point to different arrays. In practice Clang makes them different; GCC, once it implements ```plaintext define_static_array ``` , will presumably make them the same.

However, template parameter objects are invariably const! Therefore, you cannot use ```plaintext define_static_array ``` to produce a ```plaintext constinit ```

-but-mutable array, the way you can with the “constexpr two-step.” It seems to me perfectly reasonable to want a magic consteval function that says, “Please generate me a mutable array in static storage with these contents” — specified as a constexpr-time ```plaintext vector<int> ``` — “and give me a ```plaintext span ``` over it”:

``` template<class R> consteval auto define_mutable_static_storage_array(R&& r) -> std::span<std::ranges::range_value_t<R>>; ```

Perfectly reasonable to _want_ such an API; but C++26 ```plaintext define_static_array ``` fundamentally isn’t that API. It can’t produce mutable data: it can’t produce _anything_ except pointers into (potentially non-unique) template parameter objects, which behave like const inline variables.

Conclusion

In short, ```plaintext define_static_array ``` is constitutionally unsuited for some conspicuous use-cases. I’m not sure what this means for the future. I’m sure we don’t want to require people to use the “constexpr two-step” forever; but ```plaintext define_static_array ``` doesn’t seem suited to replace _all_ of its uses — certainly not in C++26, and I don’t see how it could be extended in the future to solve any of the problems I outlined above.

I imagine the answer is not “ ```plaintext define_static_array ``` will solve all your problems today,” nor “a new and improved ```plaintext define_static_array ```

will solve all your problems in C++XY,” but rather “C++XY will introduce a new and different facility for manipulating static storage” — possibly related to the as-yet-unstandardized code-generation side of reflection — and we’ll use that new facility to solve some (but perhaps not all) of the above problems.

- * *

UPDATE: Actually, problems (1), (2), and (3) all stem from ```plaintext define_static_array ```

’s requirement that each element be usable as an NTTP. Barry Revzin’s P3380R1 “Extending support for class types as NTTPs” (December 2024) lays out a plan that would permit the programmer to mark their own types as _explicitly structural_, thus (if accepted) addressing all three of those problems. On the other hand, making a user-defined type _explicitly structural_ per P3380R1 seems to involve pretty arcane programming. The “constexpr two-step” stays general by staying above the fray: it simply never requires anything to be encoded as a template argument.

Posted 2026-04-24

constexprmetaprogrammingreflection

[](mailto:arthur.j.odwyer@gmail.com)[](https://github.com/Quuxplusone)[](https://www.linkedin.com/in/arthur-odwyer)[](https://quuxplusone.github.io/blog/feed.xml)[](http://stackoverflow.com/users/1424877/quuxplusone)