Is std::vector consteval or constexpr? A reader's question, answered
A colleague asked me this in Slack right after post 4 (template for) shipped:
Is
std::vectorconsteval and not constexpr?
It’s a good question. The short answer is no: std::vector has been constexpr since C++20 — its member functions can run during constant evaluation, you can sort one in a consteval function, you can return one. But the intuition behind the question is right: in practice, a std::vector value can almost only exist during constant evaluation. It cannot survive into runtime as a constexpr namespace-scope variable. And once you understand why, the line std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx)) from post 4 stops looking like a magic incantation and starts looking like the obvious next step.
This post walks through the trio (constexpr, consteval, constinit), the transient-allocation rule that gates everything, the define_static_array “promote” step, and where the standard is heading next.
The trio in three paragraphs
constexpr does double duty. On a function (constexpr int square(int x);) it means “this function may run at compile time, and may also run at runtime.” On a variable (constexpr int N = 64;) it means “the value is fixed at compile time and usable in constant expressions.” A constexpr variable’s storage must persist for the entire program — so its initializer must be a constant expression that produces a value the compiler can write into the static data segment.
consteval applies only to functions and is strictly stronger than constexpr: every call to a consteval function must produce a constant expression. Such a function is called an immediate function. It cannot run at runtime — the compiler refuses to emit a runtime symbol for it. consteval functions can allocate memory with new — but only if they delete it before returning, because anything they leave behind has nowhere to go (more on this in a moment).
constinit applies only to static or thread-local variables and addresses a completely different problem: the static-initialization-order fiasco. A constinit variable is forced to undergo constant initialization (no dynamic init, no order dependencies on other translation units). It is not const at runtime — you can mutate it after construction. And — counter-intuitively — a constinit variable cannot itself be used in constant expressions. You also cannot combine it with constexpr or consteval: at most one of the three per declaration.
| Specifier | Applies to | Compile-time? | Holds std::vector at namespace scope? |
|---|---|---|---|
constexpr (function) | functions | maybe | n/a |
constexpr (variable) | any variable | yes (value fixed) | no — vector’s heap allocation escapes |
consteval | functions only | always | n/a — but may contain a transient vector |
constinit | static / thread_local vars | init only | no — same heap-escape problem |
Why constexpr std::vector v at namespace scope doesn’t compile
Constant evaluation in C++20 lives behind a firewall. The compiler can pretend to allocate, dereference, and free memory inside the firewall. It can sort, push back, copy. But none of that imaginary memory has any presence in the actual emitted binary. The rule, formally, is transient allocation: any memory allocated during constant evaluation must also be released during the same evaluation. If your program tries to leak compile-time storage into runtime, the compiler refuses to compile.
That is exactly what a constexpr std::vector<int> v = {1, 2, 3}; at namespace scope tries to do. The vector’s three ints live on the heap. The compiler would have to either (a) write that heap allocation into the binary’s data segment with a pointer the runtime program could observe, or (b) leave the allocation on the imaginary compile-time heap and pretend it is reachable at runtime — and (b) is incoherent. Both constexpr and constinit hit the same wall: constinit doesn’t help, because the storage problem is independent of whether the variable is also const.
Where std::vector does shine in compile-time code: as a local variable inside a constexpr or consteval function, or as a return value from one. The vector lives during the call, the compiler reads off its contents, and the storage is freed before the evaluation completes. The values can survive (copied into something else); the container cannot.
Promote: std::define_static_array (P3491)
C++26 ships a small library function that solves the “I have a transient vector and I need its values to live in static storage” problem in one line: std::define_static_array. Given a range whose elements are structural (loosely: usable as non-type template arguments), it materialises a static-storage const array holding the values, and returns a std::span<range_value_t<R> const> over it.
// Toggle: uncomment the line below to make fibs `consteval`. Default
// (commented out) leaves fibs `constexpr`, so all three patterns run.
// #define FIBS_AS_CONSTEVAL
#ifdef FIBS_AS_CONSTEVAL
# define FIBS_SPECIFIER consteval
#else
# define FIBS_SPECIFIER constexpr
#endif
FIBS_SPECIFIER auto fibs(int n) {
std::vector<int> out;
int a = 0, b = 1;
for (int i = 0; i < n; ++i) { out.push_back(a); int t = a + b; a = b; b = t; }
return out;
}
int main() {
// (1) template for over a static-storage span
template for (constexpr int x : std::define_static_array(fibs(8)))
std::println("(1) fib = {}", x);
// (2) constexpr local span: literal pointer to static storage
constexpr auto f2 = std::define_static_array(fibs(8));
for (auto x : f2) std::println("(2) fib = {}", x);
#ifndef FIBS_AS_CONSTEVAL
// (3) runtime call: real heap-allocated vector. Excluded when
// FIBS_AS_CONSTEVAL is defined -- pattern (3) is exactly the call
// site that consteval forbids, so we skip it to keep the program
// compilable in both toggle states.
auto f3 = fibs(8);
for (auto x : f3) std::println("(3) fib = {}", x);
#endif
}
Three patterns, one function. Each one teaches a different rule:
(1) template for over define_static_array(fibs(8)). This is the post-4 pattern. define_static_array is consteval, so the whole expression is evaluated in immediate-function context and fibs(8) runs at compile time regardless of whether fibs is marked constexpr or consteval. define_static_array consumes the transient vector during that same evaluation, materialises a static-storage const array of 8 ints, and returns a std::span<const int> whose .data() points into the binary. The span is a constant expression at template-instantiation time — which is exactly what template for requires.
(2) constexpr auto f2 = std::define_static_array(fibs(8));. This is the pattern you actually want most of the time. std::span is itself a literal type (just a pointer + size), so the span value is a valid constexpr initializer; the pointer it holds points to static storage (allocated by define_static_array), so it freely escapes into runtime. You get a constexpr handle you can iterate as many times as you like with zero runtime allocation. If a future translation unit consumes the same span, the linker deduplicates the storage. This is the cheap-and-honest way to ship “compile-time-known data, used at runtime.”
(3) auto f3 = fibs(8);. This compiles only because fibs is constexpr, not consteval. The call happens at runtime, allocates a real heap-backed std::vector<int>, runs the same loop, returns the vector. This is the practical difference between the two specifiers: constexpr says “I can run at compile time or at runtime”; consteval says “compile time only — if you try to call me at runtime, the program is ill-formed.” Uncomment the #define FIBS_AS_CONSTEVAL at the top of the file — patterns (1) and (2) keep working (consteval still composes with define_static_array); pattern (3) drops out via the matching #ifndef. Want to see the consteval restriction fire as an error? Uncomment the #define, then also delete the #ifndef / #endif around pattern (3) — the auto f3 = fibs(8) line then gets the diagnostic: “call to consteval function ‘fibs’ is not a constant expression.”
Could the demo self-toggle? Conceptually, yes — you would wrap pattern (3) in something like
if constexpr (! consteval-test on ^^fibs)and the body would silently disappear whenfibsis consteval, leaving patterns (1) and (2) to run. The matching reflection predicate is proposed asstd::meta::is_constevalin P3795R0 “Miscellaneous Reflection Cleanup” — but it is not yet shipped in clang-p2996, and the SFINAE idioms you might reach for don’t work either. The reason is structural: SFINAE lives in unevaluated contexts (requiresbodies,decltypeoperands), but the consteval restrictions only fire in potentially-evaluated contexts. The two never meet. Concretely:requires { F(0); },requires { +&F; },requires { static_cast<Fn*>(F); }, lambda-bodied variants, alias-templatedecltype(F(0))substitutions, and the classical Walter-Brown two-overload pair (auto probe(Fn) -> decltype(...)+probe(...)fallback) all return “callable” for both kinds. The places where consteval restrictions do fire — binding a consteval function to anautoorauto&non-type template parameter, decaying it to a function pointer in a runtime expression — produce hard errors at the call site (“pointer to a consteval declaration is not a constant expression”), not SFINAE-friendly substitution failures, so wrapping the binding inside another template just forwards the hard error. The honest reproducer for now is to flip the keyword on line one and comment out pattern (3) together. When P3795 lands, this demo gets a one-line upgrade — and reflection has officially started introspecting the qualifiers of the very functions that produced its values.
That same define_static_array step appears in post 4 (template for), where it promotes the std::vector<std::meta::info> returned by nonstatic_data_members_of into a static-storage span the expansion statement can iterate. Same mechanism, different range type.
Limits, and where this is heading
define_static_array is not magic. It works by reflecting each element as a non-type template argument, so it requires the element type to be structural and each particular value to be suitable for use as a template argument. That excludes a lot in practice: std::optional<T>, std::string_view, std::span itself, and const char* pointing at a string literal (the literal is not a template-arg-suitable address). Companions std::define_static_string (for char ranges) and std::define_static_object (for a single object) cover some of those gaps.
There is also a more general fix in flight. WG21 paper P3554R0 — “Non-transient allocation with std::vector and std::basic_string” — would let those two specific containers’ allocations persist to runtime. With it, constexpr std::vector<int> v = fibs(8); at namespace scope would just work. The paper is currently labelled for C++29 (cplusplus/papers#2201), so for C++26 we still go through define_static_array. The general non-transient-allocation problem (custom allocators, arbitrary container types — the territory of P0784 / P1974 / P2670) remains unsolved.
Decision tree
You have data X at compile time and want it at runtime. Pick:
- Scalar / aggregate of scalars — just declare a
constexprvariable. No promotion needed. - Fixed-size sequence of structural values — build a
std::arraydirectly, or build a transientstd::vectorand pipe throughstd::define_static_array. Get back astd::span<const T>. - String literal known at compile time —
std::define_static_string. Get backconst char*. - One structural object —
std::define_static_object. Get back aT const*. - Anything that needs to mutate at runtime — you don’t need any of this; declare a regular runtime variable. Reflection doesn’t change ownership rules.
- Container with non-structural element type (
std::optional,std::span, …) — today: copy element-by-element into a runtime container, OR use Jason Turner’s “constexpr two-step.” After C++29 (if P3554 lands):constexpr std::vectordirectly.
What’s next
Post 5 (enum to string) ships Tuesday and uses the same pattern at smaller scale. The Production C++ series, beginning 2026-06-16, expands this post into a deeper post on the compile-time toolbox — with allocator nuance, compile-time and binary-size benchmarks, cross-compiler quirks. Until then, when you see define_static_array in a reflection example, you know exactly what work it’s doing: turning a transient consteval value into static storage your runtime program can observe.