avatar Barry's C++ Blog

Implementing a Struct of Arrays

Posted May 2, 2025 Updated May 5, 2025 By _Barry Revzin _

最近,我观看了 Andrew Kelley 关于 Practical Data Oriented Design 的演讲。它深入探讨了他对 Zig 编译器进行的一些架构更改,带来了非常显著的性能提升。强烈建议大家观看这个演讲,即使你像我一样从未编写过任何 Zig 代码。

在演讲进行到一半时,他展示了一种通过避免浪费内存来改善内存使用的方法。通过将以下结构:

const Monster = struct {
  anim : *Animation,
  kind : Kind,
  const Kind = enum {
    snake, bat, wolf, dingo, human
  };
};
var monsters : ArrayList(Monster) = .{};

转换为这个结构:

var monsters : MultiArrayList(Monster) = .{};

ArrayList(Monster) 相当于 std::vector<Monster>,而 MultiArrayList(Monster) 现在将 animkind 存储在两个单独的数组中,而不是一个数组中。也就是说,使用 Struct of Arrays 而不是 Array of Structs。而且这只是一个 非常小的 代码更改。

对我来说,Zig 有趣的一点是,类型是一等公民。你不需要像 std::vector 这样接受模板类型参数的类模板,而是编写一个接受类型作为函数参数的函数。然后该函数返回一个类型。

MultiArrayList 的实现 实际上 是:

pub fn MultiArrayList(comptime T: type) type { return struct { // lots of code }; }


这篇博文的目标是使用 C++26 Reflection 实现相同的功能。我们将编写一个 `SoaVector<T>`,它不是 `T` 的动态数组,而是为 `T` 的每个非静态数据成员提供一个动态数组。

## We Start with Storage

为了本文的目的,我们将选择一个具有两个不同类型成员的简单类型。比方说……一个棋盘坐标:

struct Point { char x; int y; };


如果我们要实现一个简单的 `Vector<Point>`,我们的存储将如下所示:

struct { Point* data; size_t size; size_t capacity; };


但是我们正在编写一个 `SoaVector<Point>`,这意味着我们希望单独存储 `x` 和 `y`。现在,我们可以偷懒并这样做:

struct { std::vector x; std::vector y; };


这将满足要求,但这不是一个好方法。这两个 `vector` 始终具有相同的大小和容量——没有理由单独跟踪它们。虽然我不是想在这篇博客中生成一些最佳的、可用于生产环境的结构……但我们也不要过早地悲观化。

相反,我们想要这样做:

struct { // a pointer for each non-static data member char* x; int* y; // and then a size/capacity that apply to all of them size_t size; size_t capacity; };


C++26 Reflection 在代码生成方面没有提供很多功能,但它 _确实_ 提供了实现这一目标的工具。有一个函数 `std::meta::define_aggregate()`,可以让我们……定义一个聚合。通过提供我们想要生成的数据成员。

再加上查询数据成员的能力,这就是我们开始所需的全部:

template struct SoaVector { struct Pointers; consteval { define_aggregate(^^Pointers, nsdms(^^T) | std::views::transform([](std::meta::info member){ return data_member_spec(add_pointer(type_of(member)), {.name = identifier_of(member)}); })); } Pointers pointers_ = {}; size_t size_ = 0; size_t capacity_ = 0; };


在这里,`nsdms` 是一个方便的助手,因为实际的 API 很冗长:

consteval auto nsdms(std::meta::info type) -> std::vectorstd::meta::info { return nonstatic_data_members_of(type, std::meta::access_context::current()); }


然后,对于每个非静态数据成员 `T mem`,我们创建一个 `data_member_spec`,其类型为 `T*`,其名称为 `mem`。我选择将 `size_` 和 `capacity_` 成员分开,既因为这样更简单(为什么要代码生成我知道我需要的成员),又因为它具有很好的对称性(`SoaVector<T>::Pointers` 和 `T` 具有相同数量的成员),也许最重要的是,这意味着我不必担心 `T` 的任何成员的名称,以及可能与 `size_` 和 `capacity_` 冲突。

我们有了一个不错的开始。

## Let’s Add Some Elements

当然,我们的存储只是静静地坐在那里并没有什么特别之处。接下来要实现的是 `push_back`。对于我们的 `SoaVector`,`push_back` 的基本轮廓与常规 `Vector` 相同,因此我们从骨架开始:

auto push_back(T const& value) -> void { if (size_ == capacity_) { grow(/* some new capacity */); } // add this element ++size_; }


其中

auto grow(size_t new_capacity) -> void { // 1. allocate new storage // 2. copy/move into the new storage // 3. deallocate the old storage }


现在,为了简单起见并限制我们的关注点,我不会担心诸如异常安全性之类的事情,我们将只复制元素。

我们将首先从 `grow` 开始,因为它更简单。而且因为我不担心异常,所以我们实际上可以按顺序对每个非静态数据成员执行该步骤序列

auto grow(size_t new_capacity) -> void { Pointers new_pointers = {}; template for (constexpr auto M : /* ??? */) { // 1. allocate new storage // 2. copy into the new storage // 3. deallocate the old storage } pointers_ = new_pointers; capacity_ = new_capacity; }


不幸的是,C++26 的一个限制是,我们不能只在扩展语句中执行 `nsdms(^^Pointers)`。这需要非瞬态分配,而我们没有。值得庆幸的是,我们有一个库解决方案,以 `std::define_static_array()` 的形式。该函数创建一个静态存储数组,其中包含你传递给它的内容,并返回一个 `std::span<T const>` 到这些内容中。重要的是,该 `std::span` 可以用作 `constexpr` 变量(它只是指向 `static` 存储)!这是会反复出现的事情,因此我们将其存储在类本身中:

template struct SoaVector { struct Pointers; consteval { /* ... */ } Pointers pointers_ = {}; size_t size_ = 0; size_t capacity_ = 0; static constexpr auto mems = define_static_array(nsdms(^^T)); static constexpr auto ptr_mems = define_static_array(nsdms(^^Pointers)); };


这将允许我们实现 `grow`(以及一些方便的辅助函数):

auto grow(size_t new_capacity) -> void { Pointers new_pointers = {}; template for (constexpr auto M : ptr_mems) { // 1. allocate new_pointers.[:M:] = allocate<[:remove_pointer(type_of(M)):]>( new_capacity); // 2. copy std::uninitialized_copy_n(pointers_.[:M:], size_, new_pointers.[:M:]); // 3. deallocate delete_range(pointers_.[:M:]); } pointers_ = new_pointers; capacity_ = new_capacity; } template auto allocate(size_t cap) -> U* { return std::allocator().allocate(cap); } template auto delete_range(U* ptr) -> void { std::destroy(ptr, ptr + size_); std::allocator().deallocate(ptr, capacity_); }


每当我们分配内存时,我们都必须记住清理它。我们在 `grow` 中对旧存储执行此操作,并且我们也必须在析构函数中执行此操作:

~SoaVector() { template for (constexpr auto M : ptr_mems) { delete_range(pointers_.[:M:]); } }


现在我们有了内存(并且正确地清理了它),让我们回到 `push_back`。我们需要做的是获取一个 `T`,并将该 `T` 的每个成员写入相应的数组中。读取源成员需要查看 `T` 的非静态数据成员,而写入目标成员需要查看 `Pointers` 的非静态数据成员。我们可以循环访问成员的数量,也可以循环访问两个成员集合的 `zip`。

我将采用前者,因为 clang 尚未实现 `constexpr` 结构化绑定:

auto push_back(T const& value) -> void { if (size_ == capacity_) { // some exponential growth grow(std::max(3 * size_ / 2, size_ + 2)); } template for (constexpr auto I : std::views::iota(0zu, mems.size())) { constexpr auto from = mems[I]; constexpr auto to = ptr_mems[I]; using M = [: type_of(from) :]; ::new (pointers_.[: to :] + size_) M(value.[:from:]); } ++size_; }


如果我使用 `std::construct_at`,实际上会更简单,因为我不需要确定 `M`。但总的来说,我更喜欢 placement new(尤其是因为它在 C++26 中也将是 `constexpr`),因为它可以进行各种初始化。

到目前为止一切顺利。如果我将所有内容都设置为 `public` 以便于调试:

struct Point { char x; int y; }; int main() { SoaVector v; v.push_back(Point{.x='e', .y=4}); v.push_back(Point{.x='f', .y=7}); std::println("x={}", std::span(v.pointers_.x, v.size_)); // x=['e', 'f'] std::println("y={}", std::span(v.pointers_.y, v.size_)); // y=[4, 7] }


## Reading Those Elements

现在,索引是事情变得真正有趣的地方。因为我们返回什么?就本文而言,我们将以两种不同的方式进行操作:

*   `const` 索引运算符将只按值返回一个 `Point`。
*   可变索引运算符将返回一个指向 `Point` 的视图——一种新的 `PointRef` 类型。

我所说的 `PointRef` 是:

struct PointRef { char& x; int& y; auto operator=(Point const&) const -> void; // assigns through };


这里的重点(抱歉)不是要争论这是实现 `SoaVector<T>` 的绝对正确方法。也许你认为 `const` 索引运算符应该返回一个具有 `const&` 的 `PointRef` 版本。也许你认为甚至不应该有索引运算符。我不知道正确的答案是什么。但是以这种方式进行操作应该展示如何才能做到你想要做的任何事情。

但在我们进一步讨论之前,让我们为此项目添加更多可调试性(从 [这篇较早的文章](https://brevzin.github.io/c++/2025/05/02/soa/</c++/2024/09/30/annotations/#pretty-printing-a-struct>) 复制):

struct [[=derive]] Point { char x; int y; };


现在我们实际上可以打印我们的 `Point` 了。好多了!

### Indexing into a Value

我们要做的第一件事是编写

auto operator[](size_t idx) const -> T;


这将使我们能够将 `Point` `push_back` 到我们的 `SoaVector` 中,然后成功地将 `Point` 读回。这实际上是能够声称我们已经实际实现了一个 struct-of-arrays vector 的最低要求。

现在,到目前为止,我们已经看到了几个我们需要一次迭代一个成员的示例。我们一次分配/释放一个成员,我们一次写入一个成员。但是读取我们实际上不能一次执行一个成员。好吧,我们 _可以_ ——发出等效于:

auto operator[](size_t idx) const -> Point { Point p; p.x = pointers_.x[idx]; p.y = pointers_.y[idx]; return p; }


对于这种 `Point` 类型,这完全没问题。但让我们尝试更好的方法。我们想要发出:

auto operator[](size_t idx) const -> Point { return Point{pointers_.x[idx], pointers_.y[idx]}; }


在 C++26 中执行此操作的唯一方法是展开一个包。我们可以像往常一样使用 `index_sequence` 技巧来获取该包。或者我们可以通过执行一些 reflection 特定的操作来获取该包。让我们只执行后者,为了执行后者:

auto operator[](size_t idx) const -> T { return [: expand_all(ptr_mems) :] >> [this, idx]<auto... M>{ return T{pointers_.[:M:][idx]...}; }; }


这样,我们的 `push_back` 和 `operator[]` 都可以 [工作了](https://brevzin.github.io/c++/2025/05/02/soa/<https:/godbolt.org/z/od3Kq6683>):

struct [[=derive]] Point { char x; int y; }; int main() { SoaVector v; v.push_back(Point{.x='e', .y=4}); v.push_back(Point{.x='f', .y=7}); std::println("v[0]={}", v[0]); // v[0]=Point{.x='e', .y=4} std::println("v[1]={}", v[1]); // v[1]=Point{.x='f', .y=7} }


### Indexing into a Reference

让我们采取下一步。我们不想仅仅读取 `v[0]`,我们希望能够写入它。我们想要使 `v[0] = Point{.x='a', .y=8}` 工作。我们该怎么做?

首先,我们需要生成一种新类型。但是现在,我们不仅要发出:

struct PointRef { char& x; int& y; };


我们还需要一个赋值运算符和一个转换运算符。`std::meta::define_aggregate()` 不具备生成成员函数的能力——只能生成非静态数据成员。但这没关系,我们可以生成这些成员,然后在派生类中添加这些成员函数:

template struct SoaVector { private: struct Pointers; struct RefBase; consteval { define_aggregate(^^Pointers, transform_members(^^T, std::meta::add_pointer)); define_aggregate(^^RefBase, transform_members(^^T, std::meta::add_lvalue_reference)); } Pointers pointers_ = {}; size_t size_ = 0; size_t capacity_ = 0; static constexpr auto mems = define_static_array(nsdms(^^T)); static constexpr auto ptr_mems = define_static_array(nsdms(^^Pointers)); static constexpr auto ref_mems = define_static_array(nsdms(^^RefBase)); struct Ref : RefBase { auto operator=(T const& value) const -> void; }; };


赋值运算符的逻辑与我们在 `push_back` 中看到的相同,不同之处在于我们正在通过引用成员而不是索引指针成员进行写入:

struct Ref : RefBase { auto operator=(T const& value) const -> void { template for (constexpr auto I : std::views::iota(0zu, mems.size())) { this->[:ref_mems[I]:] = value.[:mems[I]:]; } } };


而且……基本上就是这样。返回 `Ref` 的索引运算符看起来几乎与返回 `T` 的索引运算符相同,我们只是初始化了不同的东西:

auto operator[](size_t idx) -> Ref { return [: expand_all(ptr_mems) :] >> [this, idx]<auto... M>{ return Ref{pointers_.[:M:][idx]...}; }; } auto operator[](size_t idx) const -> T { return [: expand_all(ptr_mems) :] >> [this, idx]<auto... M>{ return T{pointers_.[:M:][idx]...}; }; }


这给了我们:

struct [[=derive]] Point { char x; int y; }; int main() { SoaVector v; v.push_back(Point{.x='e', .y=4}); v.push_back(Point{.x='f', .y=7}); v[0] = Point{.x='a', .y=8}; std::println("v[0]={}", std::as_const(v)[0]); // v[0]=Point{.x='a', .y=8} std::println("v[1]={}", std::as_const(v)[1]); // v[1]=Point{.x='f', .y=7} }


这非常棒。

### Formatting the Reference

好吧,好吧。感觉有点不完整。我们应该能够只打印 `v[0]`,而不必打印 `std::as_const(v)[0]`!值得庆幸的是,注释也可以帮助我们。我们只需要使用它们:

template struct SoaVector { private: struct Pointers; struct [[=derive]] RefBase; consteval { /* ... */ } Pointers pointers_ = {}; size_t size_ = 0; size_t capacity_ = 0; static constexpr auto mems = define_static_array(nsdms(^^T)); static constexpr auto ptr_mems = define_static_array(nsdms(^^Pointers)); static constexpr auto ref_mems = define_static_array(nsdms(^^RefBase)); struct [[=derive]] Ref : RefBase { // ... }; };


注释 _太_ 酷了。

无论如何,这很好,因为它让我们只打印 `v[0]` 而不是 `std::as_const(v)[0]`。但它并没有完全按照 [我想要的方式](https://brevzin.github.io/c++/2025/05/02/soa/<https:/godbolt.org/z/eKKdroTbM>) 打印:

v[0]=Ref{RefBase{.x='a', .y=8}} v[1]=Ref{RefBase{.x='f', .y=7}}


如果在格式化时,我们强制转换为 `Point` 呢?这需要两件事。首先,我们需要添加这样的转换。没问题,我们已经做过两次了:

struct [[=derive]] Ref : RefBase { auto operator=(T const& value) const -> void { template for (constexpr auto I : std::views::iota(0zu, mems.size())) { this->[:ref_mems[I]:] = value.[:mems[I]:]; } } operator T() const { return [: expand_all(ref_mems) :] >> [this]<auto... M>{ return T{this->[:M:]...}; }; } };


然后,我们在我们的小型基于注释的格式化库中添加更多功能。现在我们只有 `derive<Debug>`。我们可以在那里添加更多信息——添加我们想要格式化的 _哪种_ 类型。让我们这样做。我们将添加一种新的注释类型:

struct format_as { std::meta::info type; };


我们将将其添加到 `Ref`(然后从 `RefBase` 中删除 `derive<Debug>`,因为它不再需要):

template struct SoaVector { private: struct Pointers; struct RefBase; consteval { /* ... */; } Pointers pointers_ = {}; size_t size_ = 0; size_t capacity_ = 0; // ... struct [[=derive, =format_as{^^T}]] Ref : RefBase { // ... }; };


现在我们必须教我们的基于注释的格式化程序来查找此其他注释。首先,我们确定我们要格式化的类型。这是 `format_as` 类型(如果存在):

consteval auto format_type(std::meta::info type) -> std::meta::info { auto as = annotations_of(type, ^^format_as); if (not as.empty()) { return extract<format_as>(as[0]).type; } else { return type; } }


如果我们找到 `format_as` 类型的注释,`as[0]` 仍然是 `std::meta::info` 类型——因此我们需要 `extract<format_as>()` 才能实际提取 `format_as` 值。

> 基于值的 reflection 模型的一个优点是,许多 Reflection 函数只是函数。

一旦我们有了这个函数,我们可以做的最简单的更改是更改我们的格式化程序以继承自不同的类模板:

template struct derive_formatter { constexpr auto parse(auto& ctx) { return ctx.begin(); } auto format(T const&, auto& ctx) const { // all as before } }; template requires (has_annotation(^^T, derive)) struct std::formatter : derive_formatter<[: format_type(^^T) :]> { };


我们完成了。`std::formatter<Point>` 和 `std::formatter<SoaVector<Point>::Ref>` 都继承自 `derive_formatter<Point>`,其 `format()` 采用 `Point const&`。对于 `Ref`,该隐式转换只是在进入时发生。

## A Working Implementation

完成所有这些之后,让我们看一下 [这个程序](https://brevzin.github.io/c++/2025/05/02/soa/<https:/godbolt.org/z/jjTEWdY6d>):

struct [[=derive]] Point { char x; int y; }; int main() { SoaVector v; v.push_back(Point{.x='e', .y=4}); v.push_back(Point{.x='f', .y=7}); v[0] = Point{.x='a', .y=8}; std::println("v[0]={}", v[0]); // v[0]=Point{.x='a', .y=8} std::println("v[1]={}", v[1]); // v[1]=Point{.x='f', .y=7} }


我们正在获取任意类型,从每个元素创建一个向量结构,支持将元素推入其中(我们自己处理分段拆分),从中读取元素(同上),甚至支持像原始类型一样打印的代理引用。

这里 `SoaVector` 的完整实现不到 100 行代码,外加一些其他的简短助手。仅粘贴实际实现:

template struct SoaVector { private: struct Pointers; struct RefBase; consteval { define_aggregate( ^^Pointers, transform_members(^^T, std::meta::add_pointer)); define_aggregate( ^^RefBase, transform_members(^^T, std::meta::add_lvalue_reference)); } Pointers pointers_ = {}; size_t size_ = 0; size_t capacity_ = 0; static constexpr auto mems = define_static_array(nsdms(^^T)); static constexpr auto ptr_mems = define_static_array(nsdms(^^Pointers)); static constexpr auto ref_mems = define_static_array(nsdms(^^RefBase)); struct [[=derive, =format_as{^^T}]] Ref : RefBase { auto operator=(T const& value) const -> void { template for (constexpr auto I : std::views::iota(0zu, mems.size())) { this->[:ref_mems[I]:] = value.[:mems[I]:]; } } operator T() const { return [: expand_all(ref_mems) :] >> [this]<auto... M>{ return T{this->[:M:]...}; }; } }; auto grow(size_t new_capacity) -> void { Pointers new_pointers = {}; template for (constexpr auto M : ptr_mems) { new_pointers.[:M:] = alloc<[:remove_pointer(type_of(M)):]>( new_capacity); std::uninitialized_copy_n(pointers_.[:M:], size_, new_pointers.[:M:]); delete_range(pointers_.[:M:]); } pointers_ = new_pointers; capacity_ = new_capacity; } template auto alloc(size_t cap) -> U* { return std::allocator().allocate(cap); } template auto delete_range(U* ptr) -> void { std::destroy(ptr, ptr + size_); std::allocator().deallocate(ptr, capacity_); } public: SoaVector() = default; ~SoaVector() { template for (constexpr auto M : ptr_mems) { delete_range(pointers_.[:M:]); } } auto push_back(T const& value) -> void { if (size_ == capacity_) { grow(std::max(3 * size_ / 2, size_ + 2)); } template for (constexpr auto I : std::views::iota(0zu, mems.size())) { constexpr auto from = mems[I]; constexpr auto to = ptr_mems[I]; using M = [: type_of(from) :]; ::new (pointers_.[: to :] + size_) M(value.[:from:]); } ++size_; } auto operator[](size_t idx) -> Ref { return [: expand_all(ptr_mems) :] >> [this, idx]<auto... M>{ return Ref{pointers_.[:M:][idx]...}; }; } auto operator[](size_t idx) const -> T { return [: expand_all(ptr_mems) :] >> [this, idx]<auto... M>{ return T{pointers_.[:M:][idx]...}; }; } };


当然,这仍然只是 `push_back` 和 `operator[]` 的两个重载,我甚至没有添加迭代器支持或你可能想要的任何其他函数。但这基本上已经是困难的部分了。一旦我们可以这样做,我们就证明了我们可以做任何事情。

例如,你可能希望能够让 `v.fields().x` 给你一个 `std::span<char>`(或 `std::span<char const>`)——仅仅是 `x`。我之前手动完成了该操作,只是为了能够测试实现,但这实际上是一件有用的事情。这样做只是另一轮生成类型然后填充它。

毋庸置疑,我对 Reflection _非常_ 兴奋。

## Comparison with Zig

我在这篇博客中多次写过关于 Rust 的文章。虽然我对 Rust 了解不多,但我至少读过《Rust 程序设计语言》,编写了许多小型 Rust 程序,阅读了博客,观看了演讲,并与 Rust 人员讨论了 Rust 的事情。

关于 Zig,我不能说 _任何_ 这些。我唯一看过的 Zig 演讲就是我在上面链接的那个(而且那个演讲可以说是少了一个 Zig 演讲,更多的是一个“Practical Data-Oriented Design”演讲,它在某种程度上与语言无关)。我没有读过任何 Zig 书籍或博客,没有编写任何程序,没有任何类似的东西。我浏览过这些文档。有点。所以我并不是一个很好的位置来对这里的 Zig 实现进行适当的比较。我提前为所有明显的错误道歉。

`MultiArrayList` 的 [Zig 实现](https://brevzin.github.io/c++/2025/05/02/soa/<https:/github.com/ziglang/zig/blob/master/lib/std/multi_array_list.zig>) 做了一个我没有想到的额外优化:它做了一个单一的分配——然后将该分配分成块。它的布局很简单:

pub fn MultiArrayList(comptime T: type) type { return struct { bytes: [*]align(@alignOf(T)) u8 = undefined, len: usize = 0, capacity: usize = 0, // ... }; }


> 来自 C/C++,Zig 声明语法有点刺耳——但它有很多优点。声明始终在一个方向上读取,没有螺旋。在这里,`bytes` 是一个指向未知数量的适当对齐的 (`@alignof(T)`) `u8` 的多项指针 (`[*]`),初始化为 `undefined`。Zig 还区分了多项指针 (`[*]T`) 和单项指针 (`*T`)。

现在,虽然我创建了结构体 `Pointers`(它对两个字段有一个 `char*` 和一个 `int*`),但 Zig 实现并没有这样做。相反,当它将单个分配分成块时,它仍然将所有内容保留在 `u8` 空间中:

pub const Slice = struct { ptrs: [fields.len][*]u8, len: usize, capacity: usize, };


`ptrs` 是一个指向 `u8` 的多项指针数组,每个字段一个指针。

但最终,我们需要回到类型空间。在我浏览任何代码之前,有一种正在生成的类型值得讨论:

pub const Field = meta.FieldEnum(T);


`meta.FieldEnum()` 是一个 Zig 函数,它接受一个类型并返回一个 `enum`(大致是一个 C++ `enum class`),每个成员都有一个枚举器。因此,如果我们有:

const Point = struct { x : c_char, y : i32 };


那么 `meta.FieldEnum(Point)` 将生成 `enum { x, y }`。

> 我 _真的_ 喜欢 Zig 的一点是,因为类型是一等公民,所以你可以简单地拥有接受类型并返回类型的函数。C++26 reflection 中我不喜欢的部分之一是我们生成类型的方法。现在,我必须写:

> ```
struct Pointers;
consteval {
  define_aggregate(^^Pointers,
           transform_members(^^T, std::meta::add_pointer));
}

真正 的含义和意图更类似于:

struct Pointers = transform_members(^^T, std::meta::add_pointer);


> Zig 得到了后者。即使我想编写 `AddPointers<T>`,实现它的唯一方法是执行进一步的间接层:

> ```
template <class T>
struct AddPointersImpl {
  struct type;
  consteval { transform_members(^^type, std::meta::add_pointer); }
};
template <class T>
using AddPointers = AddPointersImpl<T>::type;

此外,让我们看看 FieldEnum 实际是如何工作 的:

pub fn FieldEnum(comptime T: type) type {
  const field_infos = fields(T); // basically, @typeInfo(T).fields
  if (field_infos.len == 0) {
    // skipping for brevity
  }
  if (@typeInfo(T) == .@"union") {
    // skipping for brevity
  }
  var decls = [_]std.builtin.Type.Declaration{};
  var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
  inline for (field_infos, 0..) |field, i| {
    enumFields[i] = .{
      .name = field.name ++ "",
      .value = i,
    };
  }
  return @Type(.{
    .@"enum" = .{
      .tag_type = std.math.IntFittingRange(0, field_infos.len - 1),
      .fields = &enumFields,
      .decls = &decls,
      .is_exhaustive = true,
    },
  });
}

这在精神上与 std::meta::define_aggregate() 的工作方式非常相似,主要的区别在于我们 返回 一个新类型,而不是将我们要定义的类型作为参数传递。一个等效的 std::meta::define_enum() 应该是一个相当简单的扩展。

继续。最终 Zig 必须从一堆 u8 转换为特定类型。我们有 items() 可以做到这一点: