第24.4章:支持与采用

English

1. 到底谁实现了它

反射进了 C++26,但“在标准里”不等于“在你的编译器里”。写作时的支持表:

实现 反射 说明
GCC 16 是,库支持部分 第一个有 C++26 反射实现的主流编译器。语言特性和大部分 <meta> 已存在; 一些库设施和边角还在完成。用 -std=c++26,并查你那个构建的发布说明。
Bloomberg clang-p2996 是,参考实现 跟随提案维护的 Clang 分支。P2996 的许多例子用它演示。它不是上游 Clang;不要假设相同的 选项或相同的完整度。
EDG 是,有实现 EDG 前端实现了提案,并在 Compiler Explorer 上可用。它是前端,不是能直接塞进普通构建的 完整工具链。
MSVC 没有公开实现 写作时 MSVC 没有公开的反射实现。使用反射的代码在那里不能构建。
上游 Clang 没有完整实现 反射还没有作为完整特性进入上游 Clang。依赖它之前先查当前编译器支持表。

实际后果:反射已经可以用于实验、内部工具,以及能固定编译器的项目。它还不是那种可以放进库、 并要求所有用户的编译器都能构建的特性。

2. 特性测试宏

特性测试宏告诉你语言和库是否存在:

#if defined(__has_include)
#  if __has_include(<meta>)
#    include <meta>
#  endif
#endif

#if defined(__cpp_impl_reflection) && __cpp_impl_reflection >= 202506L && \
    defined(__cpp_lib_reflection) && __cpp_lib_reflection >= 202506L
#  define HAS_CXX26_REFLECTION 1
#else
#  define HAS_CXX26_REFLECTION 0
#endif

不要根据编译器名字猜。厂商可能发布部分实现,分支可能定义了宏却缺少你需要的函数。分别保护语言特性和 库特性;如果某个设施有自己的宏,也保护它。

特性测试宏是对特性的承诺,不是对每个边角情况的承诺。测试你实际使用的代码。CI 里一个只编译的小测试, 比一串版本号矩阵更有价值。

3. 可移植性边界

采用反射的干净方式,是把它关在一个头文件和一个接口后面:

reflection/
  detect.h        // feature macros, HAS_CXX26_REFLECTION
  enum_names.h    // enum_to_string, string_to_enum
  json.h          // generic serializer
  json_fallback.h // hand-written or macro-based implementation
  api.h           // includes one of the two, same public names

程序其余部分包含 api.h,永远看不到 ^^ 或 [: :]。 在支持反射的编译器上,实现是生成的。在不支持的编译器上,回退是原来的手写或宏代码。公开 API 相同, 所以程序其余部分不关心。

这个边界也限制编译期成本。反射项目里每个类型的头文件会拖慢每个包含它的翻译单元。把反射密集的代码 放在少量翻译单元里,向程序其余部分暴露普通函数或类型。

不要把反射放进公开 ABI。反射值是编译期句柄,不是稳定的二进制接口。如果布局稳定,生成的聚合体可以 出现在 ABI 里,但产生它们的反射机制是实现细节。

4. 今天能用什么

如果你现在就需要功能,又不能固定一个支持反射的编译器,现有工具仍然可用:

工具 做什么 限制
Boost.PFR 不用宏的聚合体字段访问和 tuple 式迭代 只支持聚合体;没有名字、没有枚举反射、没有类型生成
magic_enum 通过扫描编译器诊断来获得枚举名字和解析 编译器专用技巧,对枚举大小和 flags 有限制
宏表(X-macros) 一张列表展开成声明和访问器 文本的,没有类型信息,仍然是第二份声明
外部生成器 解析头文件或 schema 并生成 C++ 构建步骤、又一个解析器、输入变化时输出过期
编译器插件 完整访问 AST 编译器专用、跨版本脆弱、难以分发

这些技能没有白学。基于反射的实现仍然需要同样的策略决定,回退路径往往就是这些工具之一。 区别是谁写成员列表:你还是编译器。

5. 采用计划

不要从重写序列化器开始。从重复最痛、失败最容易发现的最小地方开始:

  1. 枚举名字。替换手写的 to_string switch。生成的函数容易和旧的比较, 漏掉枚举值是编译期或测试期失败。
  2. 调试打印。为内部类型生成字段打印器。它不在热路径上,输出容易检查。
  3. 有界格式的序列化。JSON 或配置文件。策略层保持显式:转义、可选字段、版本、 未知键。
  4. 绑定或 schema。只有前三步证明工具链在你的构建里稳定后,才从声明生成重复的包装。
  5. 公开 API。只有你支持的所有编译器都有完整实现,或者反射代码藏在非反射 ABI 后面时, 才做这一步。

每一步都保留回退。回退的成本很小,比发现某个支持的编译器构建不了发布版小得多。

6. “部分实现”在实践中意味着什么

部分实现通常在这些地方失败:

四者的缓解方式相同:保持反射代码小,隔离测试,为缺失的设施准备回退。不要在半成品实现上构建 反射密集的框架,然后在发布构建里发现缺口。

7. 语言的方向

C++26 的反射刻意是核心,不是最终形态。提案本身说,更强大的代码注入和生成是值得追求的方向。 已经发布的,是委员会能够规范和实现的部分:

这已经足够做序列化、枚举名字、绑定和布局检查。缺少通用代码注入,正是 define_aggregate 只产生简单聚合体的原因。未来标准可能拓宽生成侧; 心智模型——反射、查询、拼接——不会变。

8. 延伸阅读

要点

这就是本章。语言模型是三个运算符加一个库;工程工作是选择边界放在哪里,以及如何回退。