C++26 `define_static_array` 无法做的事情
Things C++26 define_static_array can't do

原始链接: https://quuxplusone.github.io/blog/2026/04/24/define-static-array/

## constexpr 数组:从两步法到 `define_static_array` 在 C++ 中,创建包含堆分配的全局 `constexpr` 容器是不可能的——`constexpr` 求值存在于“编译器的想象中”,无法产生运行时指针。虽然 C++20 允许在编译时使用 `new` 和 `delete`,但防火墙阻止在运行时访问该内存。 “constexpr 两步法”作为一种解决方法出现:在编译时计算一个 `std::vector`,然后将其内容复制到全局 `constexpr std::array` 中。C++26 引入了 `std::define_static_array`,用于更简洁的编译时数组生成,直接将数据存储到目标文件中。 然而,`define_static_array` 存在局限性。它仅支持“结构化类型”(如 `int`),难以处理指向字符串字面量的指针,并且无法处理只能移动的类型或创建可变数组——与更灵活的两步法相比。这是由于它依赖于模板参数和常量存储。 提出的更改 (P3380R1) 旨在通过允许用户定义的结构化类型来扩展 `define_static_array` 的支持,但完整的解决方案可能需要在未来的 C++ 标准中提供一种新的机制来操作静态存储。

黑客新闻 新的 | 过去的 | 评论 | 提问 | 展示 | 工作 | 提交 登录 C++26 define_static_array 无法做的事情 (quuxplusone.github.io) 18 分,由 jandeboevrie 发表于 2 小时前 | 隐藏 | 过去的 | 收藏 | 1 条评论 帮助 oseityphelysiol 发表于 25 分钟前 [–] C++ 人们擅长为自己制造问题,然后无休止地解决它们。这看起来不是一种高效的工作方式。回复 考虑申请 YC 的 2026 年夏季批次!申请截止至 5 月 4 日 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

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”!

Unfortunately, if I understand it correctly, C++26 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

std::define_static_array is defined in terms of std::meta::reflect_constant(e), which C++26 defines as std::meta::template_arguments_of(^^TCls<e>)[0] for some invented template TCls. That is, reflect_constant (and thus define_static_array) is defined only for structural types. 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 optional<int> is not a structural type. Nor are string, string_view, span itself… There are many types that can’t be materialized using define_static_array, even though they work fine with the “constexpr two-step” (Godbolt).

2. Pointers to string literals

Because reflect_constant is defined in terms of TCls<e>, not only must the type of e be structural, but each particular value e in the array must be suitable for use as a template argument. 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 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 e, we must make a copy of e ([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 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 reference_wrapper. 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

define_static_array allocates its array in rodata and gives you a span<const T> over it. This allows the compiler to do cool things, like point multiple invocations of define_static_array at the same backing array (Godbolt). In fact, the compiler is actually required to do that, because 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 01 02 03. p1 and p2 are required to point to the same byte; p3 and p4, since they point to std::array objects of different types, are required to point to different arrays. The compiler isn’t allowed to coalesce p3 and p4, the way it’s allowed to coalesce the backing arrays of differently typed initializer_lists (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 reflect_constant_array and define_static_array. These objects are permitted ([intro.object]/9.3) to overlap or be coalesced, just like initializer_lists 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 01 02 03. p1 and p2 are required to point to the same byte; p3 and p4 are permitted, but not required, to point to different arrays. In practice Clang makes them different; GCC, once it implements define_static_array, will presumably make them the same.

However, template parameter objects are invariably const! Therefore, you cannot use define_static_array to produce a 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 vector<int> — “and give me a 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 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.

In short, 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 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 “define_static_array will solve all your problems today,” nor “a new and improved 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 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.

联系我们 contact @ memedata.com