Skip to content
Tech News
← Back to articles

Things C++26 define_static_array can't do

read original get C 20 static array book → more articles
Why This Matters

This article highlights the limitations of C++26's define_static_array, emphasizing that certain container types like std::vector cannot be fully constexpr at global scope due to their reliance on heap allocations. Understanding these constraints is crucial for developers aiming to optimize compile-time computations and ensure portability of their code. The 'constexpr two-step' approach offers a workaround, enabling the use of more complex data structures in compile-time contexts.

Key Takeaways

We’ve seen previously that it’s not possible to create a constexpr global variable of container type, when that container holds a pointer to a heap allocation. It’s fine to create a global constexpr std::array , or even a std::string that uses only its SSO buffer; but you can’t create a global constexpr std::vector or 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 new and 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 std::vector<int> at constexpr time, then you can persist its contents into a global constexpr std::array of the appropriate size. The appropriate size is just the .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 — 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 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 define_static_array returns a 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”!

... continue reading