引言

盛夏到了,赤日当空,蝉吟鹊噪,绿树浓阴如盖,万物争展其生机。

静态反射方冒出头,便给 C++ 注入了久违的活力。这是一个全新的语言。新在哪里呢?当然不是反射本身,而是伴随反射而来的代码生成能力。从宏元编程,到模板元编程,再到如今的反射元编程,岁月苒苒,大家会认识到这是 C++ 新纪元的开始。

C++26 静态反射通过 Spec API 注入模型来提供反射元编程能力,尽管这只是一种略显笨重的注入机制,不如 Fragments、Token Sequences 等注入模型灵活,但至少当前就能使用,能够初步实现反射元编程了。

静态反射早在 CGP 一书的最后两章写过,变化实轻,便不絮繁。本文只回顾并补充注入模型的相关内容。

Spec API 注入模型

C++26 的 Spec API 注入模型包含两个元函数:define_aggregate()data_member_spec()。声明如下:

namespace std::meta {
struct data_member_options {
    struct name_type {
      template <typename T> requires constructible_from<u8string, T>
        consteval name_type(T &&);

      template <typename T> requires constructible_from<string, T>
        consteval name_type(T &&);
    };

    optional<name_type> name;
    optional<int> alignment;
    optional<int> bit_width;
    bool no_unique_address = false;
  };
consteval auto data_member_spec(info type,
                                data_member_options options) -> info;
template <reflection_range R = initializer_list<info>>
constevalauto define_aggregate(info type_class, R&&) -> info;
}

define_aggregate() 元函数负责为 class/struct/union 声明提供实现,data_member_spec() 元函数则负责提供注入数据成员声明的反射描述信息,通过 data_member_options_t 可以指定注入数据成员的名称、对齐、位域宽度等信息。

顾名思义,目前的代码注入元函数仅能够生成含有数据成员的聚合类型,功能有限。未来,该注入模型可以扩展更多的代码生成元函数,以支持高级的源码注入需求。

知晓了这两个元函数,便可以来看一个最基本的代码生成示例:

struct point;
consteval {
    define_aggregate(^^point, {
        data_member_spec(^^int, { .name = "x" }),
        data_member_spec(^^int, { .name = "y" }),
    });
}

int main() {
    point p{ 1, 2 };

    // x: 1, y: 2
    std::println("x: {}, y: {}", p.x, p.y);
}

这段代码借助 Spec API 注入模型自动生成了 point 类的实现。

consteval {} 称为 consteval 代码块,用来简化带有副作用的常量表达式求值,其求值必须发生且成功,并且非依赖 consteval 代码块的求值必定发生在编译器检查其后源码结构的合法性之前。

Spec API 动态注入

Spec API 注入模型的注入过程是一个整体,无法分步注入单个部分,而分步注入在动态注入场景中频频出现,学习反射元编程,不可不掌握这种注入技术。

试想一个最简单的动态注入场景:为 S 类自动生成任意个数的数据成员,以 m1m2m3……依次命名。

以下是 Spec API 对该问题的实现:

template<std::meta::info... Ms>
struct Helper {
    struct Type;
    consteval {
        define_aggregate(^^Type, { Ms... });
    }
};

template<std::meta::info... Ms>
using InjectTarget = Helper<Ms...>::Type;

consteval std::meta::info make_reflected_type(int n) {
    auto member_specs  = std::views::iota(1, n + 1)
        | std::views::transform([](int value) {
            std::array<char, 10> name_buf{ 'm' };
            auto [ptr, ec] = std::to_chars(name_buf.data() + 1, name_buf.data() + name_buf.size(), value);
            auto dms = data_member_spec(^^int, { .name = std::string_view(name_buf.data(), ptr) });
            return reflect_constant(dms);
        });
    return substitute(^^InjectTarget, member_specs);
}

inline constexpr auto N = 5;
using S = [:make_reflected_type(N):];

static_assert(
    nonstatic_data_members_of(^^S, access_context::current()).size() == N
);

以上代码将在编译期自动生成以下代码:

strct InjectTarget<Ms...> {
    int m1;
    int m2;
    int m3;
    int m4;
    int m5;
};

读过 CGP 「源码注入」一章的肯定了解过 Fragments 和 Token Sequences 注入模型,相比之下,Spec API 注入模型限制颇多,动态注入方式不够简单和直接。

在这种注入模型下,无法重复使用 define_aggregate() 为一个类提供定义,只能提前构建多个data_member_spec() 来实现动态注入。

并且,被注入声明不得包含 consteval 代码块,所以必须借助一个模板辅助类(Helper)来传递数据成员的注入参数,再通过 substitute() 元函数构建模板实例,生成注入类型。

这便是动态注入的完整流程。

结语

编译器在语法树间轻盈漫步,将冰冷的字节码编织成有温度的逻辑。那些曾经隐藏在二进制深处的元数据,如今已能够通过静态反射捕捉并控制,这打开了反射元编程的大门,也打开了开发者被束缚住的双手。

以往,C++ 对代码生成的可控性弱得可怜。从宏到模板是一次飞跃,从模板到反射又是一次飞跃,飞跃一次数十年,带来的是更加强大的元编程能力。如今,正是新一轮破晓的时刻,各种强大的工具蓄势待发。

于求新求学者,本文指引了反射元编程最核心的技术,此时注入模型稍弱,技术并不算多。纵是如此,已能够利用这些技术实现昔日无法想象的库或工具,以动态注入自动产生成千上万行代码。

更多内容,敬请期待。

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.