C++23 工作殆尽,C++26 紧随其后,Relection 也有了一些新进展,本篇来看这些更新的些许内容。
相关作者
知识似树,发枝散叶,往往只需两三人而已。欲了解一个领域,先知悉其中的几位关键人物,由此扩散挖掘,便可以快速理解该领域 80% 以上的内容。
是以本节介绍一下 SG7 中 Reflection 相关的研究人员。
先从 Wyatt Childers 说起,他是 Lock3 Software 的软件工程师,主要就是研究实现静态反射和元编程。lock3 版本的反射就是他们写的,在 C++ 反射 第四章 标准 中便是使用的这一版本进行示例编写。
而 Lock3 Software 公司的创始人是 Andrew Sutton,此人就是 C++20 Concepts 提案的作者,也是 GCC Concepts 的主要开发人员。他于 2010-2013 年间曾以博士后研究员的身份加入 TAMU(Texas A&M University),而 Bjarne Stroustrup 于 2002-2014 年间在 TAMU 担任计算机科学主席教授,两人就此相识。自 2012 年 Andrew Sutton 参与并实现 Concepts 之后,他便开始重度参与静态反射和元编程的设计与实现。
Barry Revzin 则是比较活跃的一位 C++ 标准委员会成员,参与过众多标准提案,比如 C++23 Deducing this、
if consteval
、Formatting Ranges、
ranges::fold
、
views::join_with
、
views::as_rvalue
等等。他也写过许多文章,参加过一些演讲,大家曾经肯定读过他的某些文章。
Peter Dimov 是 Boost 的活跃成员,编写并维护了许多库,例如 Assert、Bind、Describe、Lambda2、Mp11、SmartPtr、Variant2,许多库后面都进了 C++11 标准,如
shared_ptr
、
weak_ptr
、
enable_shared_from_this
、
bind
等等。Describe 就是 Boost 当中的一个 C++14 反射库,在
C++ 反射 第二章 探索
中也介绍过。
Faisal Vali,他也是比较早期的一位贡献者,基本一直有参与静态反射的工作。他参与的比较有名的特性应该是 C++17 CTAD 和 Constexpr Lambda。
Daveed Vandevoorde,这位也是 C++ 的早期贡献者,90 年代初便发明了各种基于模板的编程技术,并带到了 C++。早些年 C++ 模板参数必须在尖括号之间额外写一个空格,如
list
>
,后来不再必须,便是他的一个小贡献。他也是
C++ Templates – The Complete Guide
的主要作者,谁还没读过这本书呢?
因此,欲了解 Static Reflection,主要就是围绕这几人的相关论文和演讲进行,其中又以 Andrew Sutton 和 Wyatt Childers 的论文为主要资料,其他人的论文作为进一步挖掘的补充资料。
新的变化
与上次内容相比,本次更新并没有显著变化。
我觉得最大的变化在于实现上的新进展。之前使用的是 lock3 的反射版本,但那个已数年不曾更新,不支持最新的反射语法。而本次 EDG(Edison Design Group) 基于 P2996 对最新的反射提供了支持,新的语法已然生效。EDG 就是 Daveed Vandevoorde 所在的公司,他是主要技术领导之一,该公司专门研发编译器相关的技术。
因此新的提案提供了许多反射的使用示例,在应用方面更加全面。
尽管如此,EDG 的反射版本并不像 lock3 那样,支持并驾齐驱的几个元编程特性,例如 Expansion statements 和 Soure code injection,它只是在反射特性上有了更新的实现。虽然也有平替方式,但是功能上要弱化许多,而且不甚方便。
基础内容本篇不再赘述,请阅读 C++ 反射 第四章 标准,本篇接下来将介绍大量实践内容。
新的环境
新的 EDG Experimental Reflection 可以直接在 https://godbolt.org/z/beT7ao7h1 使用,以下的所有示例也全部基于该版本实现。
默认使用 C++23,头文件只需要包含
#include
,所有元函数都在
std::meta
下面。
下面是一个最简单的例子:
1#include
2
3int main() {
4 constexpr auto r = ^int;
5 typename[:r:] x = 42; // Same as: int x = 42;
6 typename[:^char:] c = '*'; // Same as: char c = '*';
7}
这里展示了 reflection 和 splicing 操作,在 lock3 版本时,囿于实现,没能使用这种最新的反射语法,而如今已经可以使用,所以 EDG Reflection 使用起来反而比 lock3 简单。
下面看更多使用例子,都是提案中的,由于例子已经很多,额外补充的不在本篇讲。
Selecting Members
这是一个操纵成员的小例子:
1struct S { unsigned i:2, j:6; };
2
3consteval auto member_number(int n) {
4 if (n == 0) return ^S::i;
5 else if (n == 1) return ^S::j;
6}
7
8int main() {
9 S s{0, 0
};
10 s.[:member_number(1):] = 42; // Same as: s.j = 42;
11 s.[:member_number(5):] = 0; // Error (member_number(5) is not a constant).
12}
通过 lifting(reflection) operator 先返回反射类型
meta::info
,再通过 splicing 重新得到成员类型,从而访问成员。
List of Types to List of Sizes
类型列表转换为类型大小列表:
1constexpr std::array types = {^int, ^float, ^double};
2
3// the consteval is required here because consteval propagation (P2564) is not yet implemented
4constexpr std::array sizes = []() consteval {
5 std::array<std::size_t, types.size()> r;
6 std::transform(types.begin(), types.end(), r.begin(), std::meta::size_of);
7 return r;
8}();
9
10static_assert(sizes[0] == sizeof(int));
11static_assert(sizes[1] == sizeof(float));
12static_assert(sizes[2] == sizeof(double));
这个例子同样很简单,不多讲,最终
sizes
的内容就相当于:
1std::array<std::size_t, 3> sizes = {sizeof(int), sizeof(float), sizeof(double)};
Implementing `make_integer_sequence`
通过反射来简化实现
make_integer_sequence
:
1template<typename T>
2consteval std::meta::info make_integer_seq_refl(T N) {
3 std::vector args{^T};
4 for (T k = 0; k 5 args.push_back(std::meta::reflect_value(k));
6 }
7 return substitute(^std::integer_sequence, args);
8}
9
10template<typename T, T N>
11 using make_integer_sequence = [:make_integer_seq_refl(N):];
12
13static_assert(std
::same_as<
14 make_integer_sequence<int, 10>,
15 std::make_integer_sequence<int, 10>
16 >);
这个实现的逻辑也比较清晰,主要涉及两个元函数,
reflect_value
和
substitude
。
其中,
reflect_value
的声明为:
1namespace std::meta {
2 template<typename T>
3 consteval auto reflect_value(T const&)->info;
4
5 template<typename R>
6 consteval auto reflect_values(R const&)->std::span;
7}
这两个元函数用于将 Constant value(s) lifting 为反射类型(meta::info)表示,比如:
1constexpr std::vector<int> v{ 1, 2, 3 };
2constexpr std::span<std::meta::info> rv = reflect_values(v);
随后,便可以将这个 lifted sequence 重新 Splicing 出来使用,如作为模板参数使用:
1std::integer_sequence<int, ...[:rv:]...> is123;
2// same as std::integer_sequence
以上仅是示例,EDG Reflection 尚不支持
reflect_values
,只支持
reflect_value
。
因此,
1args.push_back(std::meta::reflect_value(k));
的意思,就是生成一个常量序列,再通过生成的序列创建一个
std::integer_sequence
,这需要用到
substitute
元函数,其标准声明为:
1namespace std::meta {
2 consteval auto substitute(info templ, std::span args)
3 ->info { ... };
4}
功能是根据已有类型,提供参数,生成新的类型。一个例子:
1using namespace std::meta;
2template<typename ... Ts> struct X {};
3template<> struct X {};
4constexpr info type = ^X<int
, int, float>;
5constexpr info templ = template_of(type);
6constexpr span args = template_arguments_of(type);
7constexpr info new_type = substitute(templ, args.subspan(0, 2));
8typename[:new_type:] xii; // Type X, which selects the specialization.
9 // There is no mechanism to instantiate a primary template
10 // definition that is superseded by an explicit/partial
11 // specialization.
根据
X
生成了新的类型
X
。
但是,EDG 目前有些局限,它使用
std::vector
来代替
std::span
,因此
1substitute(^std::integer_sequence, args);
中才使用
std::vector
来作为参数。
Getting Class Layout
使用反射来获取类布局信息:
1struct member_descriptor
2{
3 std::size_t offset;
4 std::size_t size;
5 bool operator==(member_descriptor const&) const = default;
6};
7
8// returns std::array
9template <typename S>
10consteval auto get_layout() {
11 constexpr size_t N = []() consteval {
12 return nonstatic_data_members_of(^S).size();
13 }();
14
15 std::array layout;
16 [: expand(nonstatic_data_members_of(^S)) :] >> [&, i=0]<auto e>() mutable {
17 layout[i] = {.offset=offset_of(e), .size=size_of(e)};
18 ++i;
19 };
20 return layout;
21}
22
23struct X
24{
25 char a;
26 int b;
27 double c;
28};
29
30constexpr auto Xd = get_layout();
31static_assert(Xd.size() == 3
);
32static_assert(Xd[0] == member_descriptor{.offset=0, .size=1});
33static_assert(Xd[1] == member_descriptor{.offset=4, .size=4});
34static_assert(Xd[2] == member_descriptor{.offset=8, .size=8});
get_layout()
是主要逻辑点,用于获取一个类型的非静态数据成员信息,信息保存在
member_descriptor
当中。
由于 EDG 目前不支持 Expansion statements,所以增加了一些实现的复杂度。如果使用 Expansion statements,核心语句将可以这样实现:
1std::array layout;
2int i = 0;
3template for (constexpr auto e : std::meta::nonstatic_data_members_of(^S)) {
4 layout[i] = {.offset=offset_of(e), .size=size_of(e)};
5 ++i;
6}
expand()
是 EDG 对 Expansion statements 的临时平替,实现为:
1namespace __impl {
2 template<auto... vals>
3 struct replicator_type {
4 template<typename F>
5 constexpr void operator>>(F body) const {
6 (body.template operator()(), ...);
7 }
8 };
9
10 template<auto... vals>
11 replicator_type replicator = {};
12}
13
14template<typename R>
15consteval auto expand(R range) {
16 std::vector<std::meta::info> args;
17 for (auto r : range) {
18 args.push_back(reflect_value(r));
19 }
20 return substitute(^__impl::replicator, args);
21}
例子中其他使用的元函数皆顾名思义,逻辑清晰,不再多讲。
Enum to String
最经典的例子,相当于反射界的 Hello world。
过去的文章中已经展示了各种实现,最经典的当属标准的版本:
1template <typename E>
2 requires std::is_enum_v
3constexpr std::string
enum_to_string(E value) {
4 template for (constexpr auto e : std::meta::members_of(^E)) {
5 if (value == [:e:]) {
6 return std::string(std::meta::name_of(e));
7 }
8 }
9
10 return "";
11}
12
13enum Color { red, green, blue };
14static_assert(enum_to_string(Color::red) == "red");
15static_assert(enum_to_string(Color(42)) == "");
及反操作版本:
1template <typename E>
2 requires std::is_enum_v
3constexpr std::optional string_to_enum(std::string_view name) {
4 template for (constexpr auto e : std::meta::members_of(^E)) {
5 if (name == std::meta::name_of(e)) {
6 return [:e:];
7 }
8 }
9
10 return std::nullopt;
11}
但是 EDG 不支持 Expansion statements,所以使用
expand()
代替:
1template<typename E>
2 requires std::is_enum_v
3constexpr std::string enum_to_string(E value) {
4 std::string result = "";
5 [:expand(std::meta::enumerators_of(^E)):] >>
6 [&]<auto e>{
7 if (value == [:e:]) {
8 result = std::meta::name_of(e);
9 }
10 };
11 return result;
12}
13
14enum Color { red, green, blue };
15static_assert(enum_to_string(Color::red) == "red");
16static_assert(enum_to_string(Color(42)) == ""
);
这种实现的复杂度为
O(N)
,他们提供了另一种利用 Ranges 算法的实现,只需要
O(log(N))
的复杂度:
1template <typename E>
2 requires std::is_enum_v
3constexpr std::string enum_to_string(E value) {
4 constexpr auto enumerators =
5 std::meta::members_of(^E)
6 | std::views::transform([](std::meta::info e){
7 return std::pairstd::string>(std::meta::value_of(e), std::meta::name_of(e));
8 })
9 | std::ranges::to<std::map>();
10
11 auto it = enumerators.find(value);
12 if (it != enumerators.end()) {
13 return it->second;
14 } else {
15 return "";
16 }
17}
这种方式借助
std::map
来实现,曾经在
C++ 反射 第二章 探索
也介绍过。
A Simple Tuple Type
与传递递归继承实现法相比,一种更简单的
Tuple
实现法:
1namespace std::meta {
2 consteval auto make_nsdm_description(info type, nsdm_options options = {}) {
3 return nsdm_description(type, options);
4 }
5}
6
7template<typename... Ts> struct Tuple {
8 struct storage;
9
10 static_assert(is_type(define_class(^storage, {make_nsdm_description(^Ts)...})));
11 storage data;
12
13 Tuple(): data{} {}
14 Tuple(Ts const& ...vs): data{ vs... } {}
15};
16
17template<typename... Ts>
18 struct std::tuple_size>: public integral_constant<size_t, sizeof...(Ts)> {};
19
20template<std::size_t
I, typename... Ts>
21struct std::tuple_element> {
22 static constexpr std::array types = {^Ts...};
23 using type = [: types[I] :];
24};
25
26consteval std::meta::info get_nth_nsdm(std::meta::info r, std::size_t n) {
27 return nonstatic_data_members_of(r)[n];
28}
29
30template<std::size_t I, typename... Ts>
31 constexpr auto get(Tuple &t) noexcept -> std::tuple_element_t>& {
32 return t.data.[:get_nth_nsdm(^decltype(t.data), I):];
33 }
34
35template<std::size_t I, typename... Ts>
36 constexpr auto get(Tuple const&t) noexcept -> std::tuple_element_t> const& {
37 return t.data.[:get_nth_nsdm(^decltype(t.data), I):];
38 }
39
40template<std::size_t I, typename... Ts>
41 constexpr auto get(Tuple &&t) noexcept -> std::tuple_element_t> && {
42 return std::move(t).data.[:get_nth_nsdm(^decltype(t.data), I):];
43 }
44
45int main() {
46 auto [x, y, z] = Tuple{1, 'c', 3.14};
47 assert(x == 1);
48 assert(y == 'c');
49 assert(z == 3.14);
50}
能这样实现的关键在于代码生成,而 EDG 当前并不支持 Source code injection,所以他们提供了丐版的替代元函数
std::meta::nsdm_description
和
std::meta::define_class
,允许合成简单的
struct/union
类型。声明为:
1namespace std
::meta {
2 struct nsdm_options_t {
3 optional name;
4 optional<int> alignment;
5 optional<int> width;
6 };
7 consteval auto nsdm_description(info type, nsdm_options options = {}) -> info;
8 consteval auto define_class(info class_type, spanconst>) -> info;
9}
nsdm_description
返回给定类型非静态数据成员的反射描述信息,
nsdm_options_t
用于指定数据成员的额外信息,比如名称、对齐和宽度,而
define_class
接受一个 Incomplete
class/struct/union
和非静态数据成员的反射元信息序列(由
nsdm_description
的返回值构成),这些非静态数据成员将注入到将生成的类型里面。这就是 Source code injection 的基本能力,弱化版的实现。
举个例子:
1template<typename T> struct S;
2constexpr auto U = define_class(^S<int>, {
3 nsdm_description(^int, {.name="i", .align=64}),
4 nsdm_description(^int, {.name="j", .align=64}),
5});
6
7// S is now defined to the equivalent of
8// template<> struct S {
9// alignas(64) int i;
10// alignas(64) int j;
11// };
为
S
自动生成的非静态数据成员,如果不指定
nsdm_options_t
,那么生成的数据成员名称默认为
_0
,
_1
,
_2
……
回到
Tuple
的实现,传统方法一个是递归继承,一个是递归复合,后者实现时存在许多问题,因此一般利用前者实现。而利用反射的代码生成能力,可以直接合成一个
storage
内部类,所有
Tuple
元素全部注入到该内部类当中,便可以轻易地生成一个
Tuple
类。
借助反射,
std::tuple_element
的实现也变得非常简单:
1template<std::size_t I, typename... Ts>
2struct std::tuple_element> {
3 static constexpr std::array types = {^Ts...};
4 using type = [: types[I] :];
5};
std::get
的实现同样简单:
1consteval std::meta::info get_nth_nsdm(std::meta::info r, std::size_t
n) {
2 return nonstatic_data_members_of(r)[n];
3}
4
5template<std::size_t I, typename... Ts>
6 constexpr auto get(Tuple &t) noexcept -> std::tuple_element_t>& {
7 return t.data.[:get_nth_nsdm(^decltype(t.data), I):];
8 }
通过反射,可以直接操纵类型元信息,不再需要额外的奇技淫巧去递归地获取这些信息。
Struct to Struct of Arrays
这也是一个代码生成的例子:
1namespace std::meta {
2 consteval auto make_nsdm_description(info type, nsdm_options options = {}) {
3 return nsdm_description(type, options);
4 }
5}
6
7template <typename T, std::size_t N>
8struct struct_of_arrays_impl;
9
10consteval auto make_struct_of_arrays(std::meta::info type,
11 std::meta::info N) -> std::meta::info {
12 std::vector<std::meta::info> old_members = nonstatic_data_members_of(type);
13 std::vector<std::meta::nsdm_description> new_members = {};
14 for (std::meta::info member : old_members) {
15 auto array_type = substitute(^std::array, {type_of(member), N });
16 auto mem_descr = make_nsdm_description(array_type, {.name = name_of(member)});
17 new_members.push_back(mem_descr);
18 }
19 return std::meta::define_class(
20 substitute(^struct_of_arrays_impl, {type, N}),
21 new_members);
22}
23
24template <typename T, size_t N>
25using struct_of_arrays = [: make_struct_of_arrays(^T, ^N) :];
26
27struct point {
28 float x;
29 float y;
30 float z;
31};
32
33int main()
{
34 using points = struct_of_arrays2>;
35
36 points p = {
37 .x={1.1, 2.2},
38 .y={3.3, 4.4},
39 .z={5.5, 6.6}
40 };
41 static_assert(p.x.size() == 2);
42 static_assert(p.y.size() == 2);
43 static_assert(p.z.size() == 2);
44
45 for (size_t i = 0; i != 2; ++i) {
46 std::cout <"p[" <"] = (" <", " <", " <")\n";
47 }
48}
49
50// Output:
51// p[0] = (1.1, 3.3, 5.5)
52// p[1] = (2.2, 4.4, 6.6)
使用的都是之前介绍过的元函数,逻辑也很清晰,就是把当前结构体类型的所有非静态数据成员获取出来,再根据这些信息重新生成数组形式的成员。
最后生成的
points
相当于:
1using points = struct_of_arrays2>;
2// equivalent to:
3// struct points {
4// std::array x;
5// std::array y;
6// std::array z;
7// };
Parsing Command-Line Options
再来看一个利用反射仿 Rust clap(Command Line Argument Parser) 的实现,clap 是 Rust 的命令行参数解析器。
最终效果为:
1struct Args : Clap {
2 Option<std::string, {.use_short=true, .use_long=true}> name;
3 Option<int, {.use_short=true, .use_long=true}> count = 1;
4};
5
6int main(int argc, char** argv) {
7 auto opts = Args{}.parse(argc, argv);
8
9 for (int i = 0; i // opts.count has type int
10 std::print("Hello {}!", opts.name); // opts.name has type std::string
11 }
12}
例子中定制的
Args
支持两种参数,一个是
name
,一个是
count
,后者具有默认值。如果编译参数为:
1./test -n WG21 -c 7
-n
就对应于
name
,
-c
对应于
count
。那么输出结果将为:
1Hello WG21!
2Hello WG21!
3Hello WG21!
4Hello WG21!
5Hello WG21!
6Hello WG21!
7Hello WG21!
你可以在
Args
中定制自己的参数列表,所有的解析操作都封装在
Clap
当中。
要实现这样的效果,首先需要定义
Flags
和
Option
。
1struct Flags {
2 bool use_short;
3 bool use_long;
4};
5
6template <typename T, Flags flags>
7struct Option {
8 std::optional initializer;
9
10 Option() = default;
11 Option(T t) : initializer(t) { }
12
13 static constexpr bool use_short = flags.use_short;
14 static constexpr bool use_long = flags.use_long;
15};
Flags
用于表示参数的形式,比如短形式为
-n
,长形式就为
--name
,根据不同的形式进行不同方式的解析。
Option
用于表示定制的可选参数,有两个构造函数,表示参数值的初始化是可选的。比如只写
./test -n WG21
,此时
count
提供默认初始化为
1
,从而简化参数。
接着,定义解析方式
Clap
:
1struct Clap {
2 template <typename Spec>
3 auto parse(this Spec const& spec, int argc, char** argv) {
4 // ...
5 }
6};
这里使用了 C++23 Deducing this 作为定制点表示方式,从而简化传统的 CRTP 方式。
argc
和
argv
被传递进来,下一步操作:
1template <typename Spec>
2auto Clap::parse(this Spec const& spec, int argc, char** argv) {
3 std::vector<std::string_view> cmdline(argv + 1, argv + argc);
4
5 // check if cmdline contains --help, etc.
6
7 struct Opts;
8 static_assert(is_type(spec_to_opts(^Opts, ^Spec)));
9 Opts opts;
10
11 // ...
如果参数列表为
./test -n WG21 -c 7
,那么除了第一个参数,剩余的实际参数都被保存到
cmdline
中,所以
cmdline
的大小为 4。
紧接着开始解析,先通过代码生成自动生成
Opts
类,这个类作为解析的结果,也就是
auto opts = Args{}.parse(argc, argv);
中的
opts
类型。这个返回类型根据用户自定义的
Args
类中的非静态数据成员自动生成,生成后的结构为:
1struct Opts { std::string name; int count; };
生成工作通过
spec_to_opts
完成,实现为:
1consteval auto spec_to_opts(std::meta::info opts, std::meta::info spec) -> std::meta::info {
2 std::vector<std::meta::nsdm_description> new_members;
3 for (auto member : nonstatic_data_members_of(spec)) {
4 auto new_type = template_arguments_of(type_of(member))[0];
5 new_members.push_back(make_nsdm_description(new_type, {.name=name_of(member)}));
6 }
7 return define_class(opts, new_members);
8}
逻辑不算复杂,就是使用前面介绍过的
nsdm_description
和
define_class
来完成简单类型的代码生成工作。
因为不支持 Expansion statements,因此下一步需要借助新类型
Z
和
expand()
来进行参数遍历。
1template <typename Spec>
2auto Clap::parse(this Spec const& spec, int argc, char