第24.3章:反射实践

English

1. 枚举名字,两个方向

第一页的枚举例子值得完整写出来,因为它展示了不完整枚举的保护和反方向:

#include <meta>
#include <optional>
#include <string_view>
#include <type_traits>

template <typename E>
    requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value)
{
    if constexpr (std::meta::is_enumerable_type(^^E))
    {
        template for (constexpr auto e :
                      std::define_static_array(std::meta::enumerators_of(^^E)))
        {
            if (value == [:e:])
            {
                return std::meta::identifier_of(e);
            }
        }
    }
    return "<unnamed>";
}

template <typename E>
    requires std::is_enum_v<E>
constexpr std::optional<E> string_to_enum(std::string_view name)
{
    if constexpr (std::meta::is_enumerable_type(^^E))
    {
        template for (constexpr auto e :
                      std::define_static_array(std::meta::enumerators_of(^^E)))
        {
            if (name == std::meta::identifier_of(e))
            {
                return [:e:];
            }
        }
    }
    return std::nullopt;
}

is_enumerable_type 是前置声明枚举的保护。枚举可以在枚举值已知之前声明;这时查询 enumerators_of 是错误。if constexpr 让函数在两种情况下都能编译, 列表不可用时返回回退值。

生成的代码是一串比较,每个枚举值一个。它不是哈希表,也不分配。小枚举正是你想要的。大枚举可以 构建静态查找表:

template <typename E>
    requires std::is_enum_v<E>
constexpr auto make_enum_table()
{
    constexpr auto names = std::define_static_array(
        std::meta::enumerators_of(^^E));
    std::array<std::pair<E, std::string_view>, names.size()> table{};
    template for (constexpr std::size_t i{}; auto e : names)
    {
        table[i] = {[:e:], std::meta::identifier_of(e)};
    }
    return table;
}

表是普通的 constexpr 数组。你可以排序、二分查找,或在后续构建步骤里生成完美哈希。 反射给你数据;算法是普通 C++。

2. 字段访问器

通用字段打印器是反射的“hello world”:

#include <meta>
#include <print>

template <typename T>
void print_fields(T const &obj)
{
    constexpr auto ctx = std::meta::access_context::current();
    constexpr auto members = std::define_static_array(
        std::meta::nonstatic_data_members_of(^^T, ctx));

    template for (constexpr auto m : members)
    {
        std::print("{} = {}\n",
                   std::meta::identifier_of(m),
                   obj.[:m:]);
    }
}

它对任何成员可打印的聚合体都有效。它不跳过任何东西:静态成员不在 nonstatic_data_members_of 里,但基类不会自动遍历。如果你需要基类,对 bases_of 递归。

生成的代码正是你手写的:

std::print("name = {}\n", obj.name);
std::print("age = {}\n", obj.age);
std::print("height = {}\n", obj.height);

区别是加成员不需要改打印器。编译器看到新成员并生成新行。这就是全部投资回报。

3. JSON:完整的小型序列化器

JSON 是很好的压力测试,因为它需要名字、类型和递归。这里是一个紧凑版本,支持字符串、数字、布尔和 嵌套聚合体:

#include <concepts>
#include <meta>
#include <string>
#include <string_view>
#include <type_traits>

template <typename T>
std::string to_json(T const &obj);

inline void append_json(std::string &out, std::string const &v)
{
    out += '"';
    out += v;
    out += '"';
}

inline void append_json(std::string &out, std::string_view v)
{
    out += '"';
    out += v;
    out += '"';
}

inline void append_json(std::string &out, char const *v)
{
    append_json(out, std::string_view{v});
}

inline void append_json(std::string &out, bool v)
{
    out += v ? "true" : "false";
}

template <std::integral T>
void append_json(std::string &out, T v)
{
    out += std::to_string(v);
}

template <std::floating_point T>
void append_json(std::string &out, T v)
{
    out += std::to_string(v);
}

template <typename T>
void append_json(std::string &out, T const &v)
{
    out += to_json(v);
}

template <typename T>
std::string to_json(T const &obj)
{
    std::string out{"{"};
    constexpr auto ctx = std::meta::access_context::current();
    constexpr auto members = std::define_static_array(
        std::meta::nonstatic_data_members_of(^^T, ctx));

    bool first{true};
    template for (constexpr auto m : members)
    {
        if (!first)
        {
            out += ',';
        }
        first = false;
        out += '"';
        out += std::meta::identifier_of(m);
        out += "\":";
        append_json(out, obj.[:m:]);
    }
    out += '}';
    return out;
}

重载集是策略层。反射提供成员名字和字段访问;重载决定 std::string、int 或嵌套聚合体如何变成 JSON。加一个类型就加一个重载,不是加一份成员列表。

这个序列化器刻意忽略了转义。真正的 JSON 写入器必须转义引号、反斜杠和控制字符,拒绝非有限浮点值, 并决定未知类型怎么办。反射不替你做这些决定。它只删除“遍历字段”的样板。

4. 生成配置类型

当类型的形状是数据时,define_aggregate 很有用。配置 schema、数据库行、固定字段列表的 协议消息——这些都可以描述成成员列表,然后变成真正的 C++ 聚合体:

#include <meta>
#include <string>

struct config;

consteval
{
    std::meta::info fields[]
    {
        std::meta::data_member_spec(^^std::string, {.name = "host"}),
        std::meta::data_member_spec(^^int, {.name = "port"}),
        std::meta::data_member_spec(^^bool, {.name = "tls"})
    };

    std::meta::define_aggregate(^^config, fields);
}

int main()
{
    config c{.host = "example.com", .port = 443, .tls = true};
    return c.port == 443 ? 0 : 1;
}

schema 驱动的生成器会在构建时从文件读取字段列表,但编译期核心是一样的:构建 std::vector<std::meta::info> 的 data_member_spec 结果, 然后调用 define_aggregate。结果是普通聚合体,可以用指定初始化器,也可以配合上面的 JSON 序列化器。

限制很重要:define_aggregate 产生简单聚合体。它不生成构造函数、成员函数、虚函数、 访问说明符或基类。如果生成的类型需要行为,写一个单独的包装或接受该聚合体的自由函数。

5. 布局与 ABI 检查

反射可以在编译期回答布局问题:

template <typename T>
consteval bool has_padding()
{
    constexpr auto ctx = std::meta::access_context::current();
    auto members = std::meta::nonstatic_data_members_of(^^T, ctx);
    std::size_t end{};
    for (auto m : members)
    {
        auto off = static_cast<std::size_t>(std::meta::offset_of(m).bytes);
        auto size = std::meta::size_of(m);
        if (off != end)
        {
            return true;
        }
        end = off + size;
    }
    return end != std::meta::size_of(^^T);
}

这不是平台 ABI 文档的替代品。它是编译期守卫:如果必须与线格式或 C ABI 布局兼容的结构体变了, 断言在构建时触发,而不是在运行时产生损坏的包。

同样的数据支持生成描述、对偏移的静态断言,或比较 C++ 布局与 C 头文件的测试。反射把 “我们希望编译器这样布局”变成编译器执行的检查。

6. 设计反射库

生产级反射库主要是策略,不是语法。有效的结构是:

  1. 一个通用遍历。反射成员一次;对每个成员调用定制点。不要在每种序列化器里 写一个单独的循环。
  2. 一个定制点。一个函数或 concept,说明如何处理叶子类型。遍历决定顺序; 定制决定表示。
  3. 基类单独遍历。nonstatic_data_members_of 不包含基类。 如果格式是扁平的,用 bases_of 显式访问基类并递归。
  4. 用注解表达策略。注解可以标记字段为可选、重命名、保密或带版本。遍历读取它们; 序列化器遵守它们。注解词汇保持小。
  5. 表用静态存储。当生成的表必须活过单个常量表达式时,用 define_static_array 或 define_static_string。

反模式是每种格式一个反射循环。那只是多绕几步的手写代码。反射的意义是一个遍历、多种格式。

7. 编译期成本与调试

反射是编译期工作。每个翻译单元都反射大类型图的头文件会拖慢构建,深度递归的序列化器会撞上 constexpr 求值限制。几个习惯能让它可控:

调试反射不同于调试模板。错误通常出现在拼接处,而不是产生坏反射的查询处。在拼接前对 is_class_type、is_enum_type 或 has_identifier 做一个小 static_assert,能把一墙错误变成一条清晰消息。

要点

最后一页讲编译器支持、特性测试宏、当前实现的限制,以及如何在不把构建押上去的情况下采用反射。