Reflection for C++26

教育   科技   2024-02-10 14:14   陕西  

岁月如驰,距上次写反射文章,已近两载。

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::foldviews::join_withviews::as_rvalue 等等。他也写过许多文章,参加过一些演讲,大家曾经肯定读过他的某些文章。

Peter Dimov 是 Boost 的活跃成员,编写并维护了许多库,例如 Assert、Bind、Describe、Lambda2、Mp11、SmartPtr、Variant2,许多库后面都进了 C++11 标准,如 shared_ptrweak_ptrenable_shared_from_thisbind 等等。Describe 就是 Boost 当中的一个 C++14 反射库,在 C++ 反射 第二章 探索 中也介绍过。

Faisal Vali,他也是比较早期的一位贡献者,基本一直有参与静态反射的工作。他参与的比较有名的特性应该是 C++17 CTAD 和 Constexpr Lambda。

Daveed Vandevoorde,这位也是 C++ 的早期贡献者,90 年代初便发明了各种基于模板的编程技术,并带到了 C++。早些年 C++ 模板参数必须在尖括号之间额外写一个空格,如 list<complex<double> >,后来不再必须,便是他的一个小贡献。他也是 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 <experimental/meta>,所有元函数都在 std::meta 下面。

下面是一个最简单的例子:

1#include <experimental/meta>
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 == 0return ^S::i;
5  else if (n == 1return ^S::j;
6}
7
8int main() {
9  S s{00};
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 < N; ++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<T>(N):];
12
13static_assert(std::same_as<
14    make_integer_sequence<int10>,
15    std::make_integer_sequence<int10>
16    >);

这个实现的逻辑也比较清晰,主要涉及两个元函数,reflect_valuesubstitude

其中,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<info>;
7}

这两个元函数用于将 Constant value(s) lifting 为反射类型(meta::info)表示,比如:

1constexpr std::vector<int> v{ 123 };
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<int, 1, 2, 3>

以上仅是示例,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<info> args)
3    ->info 
{ ... };
4}

功能是根据已有类型,提供参数,生成新的类型。一个例子:

 1using namespace std::meta;
2template<typename ... Ts> struct X {};
3template<> struct X<int, int> {};
4constexpr info type = ^X<intintfloat>;
5constexpr info templ = template_of(type);
6constexpr span<info> args = template_arguments_of(type);
7constexpr info new_type = substitute(templ, args.subspan(02));
8typename[:new_type:] xii; // Type X<int, int>, 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<int, int, float> 生成了新的类型 X<int, int>

但是,EDG 目前有些局限,它使用 std::vector<info> 来代替 std::span<info const>,因此

1substitute(^std::integer_sequence, args);

中才使用 std::vector<info> 来作为参数。

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<member_descriptor, N>
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<member_descriptor, N> 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<X>();
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<member_descriptor, N> 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()<vals>(), ...);
7      }
8  };
9
10  template<auto... vals>
11  replicator_type<vals...> 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<E>
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 "<unnamed>";
11}
12
13enum Color { red, green, blue };
14static_assert(enum_to_string(Color::red) == "red");
15static_assert(enum_to_string(Color(42)) == "<unnamed>");

及反操作版本:

 1template <typename E>
2  requires std::is_enum_v<E>
3constexpr std::optional<E> 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<E>
3constexpr std::string enum_to_string(E value) {
4  std::string result = "<unnamed>";
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)) == "<unnamed>");

这种实现的复杂度为 O(N),他们提供了另一种利用 Ranges 算法的实现,只需要 O(log(N)) 的复杂度:

 1template <typename E>
2  requires std::is_enum_v<E>
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::pair<E, std::string>(std::meta::value_of<E>(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 "<unnamed>";
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<Tuple<Ts...>>: public integral_constant<size_tsizeof...(Ts)> {};
19
20template<std::size_t I, typename... Ts>
21struct std::tuple_element<I, Tuple<Ts...>> {
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<Ts...> &t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>>& {
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<Ts...> const&t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>> 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<Ts...> &&t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>> && {
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_descriptionstd::meta::define_class,允许合成简单的 struct/union 类型。声明为:

1namespace std::meta {
2  struct nsdm_options_t {
3    optional<string_view> 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, span<info const>) -> 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<int> is now defined to the equivalent of
8// template<> struct S<int> {
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<I, Tuple<Ts...>> {
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<Ts...> &t) noexcept -> std::tuple_element_t<I, Tuple<Ts...>>& {
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_arrays<point, 2>;
35
36    points p = {
37        .x={1.12.2},
38        .y={3.34.4},
39        .z={5.56.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[" << i << "] = (" << p.x[i] << ", " << p.y[i] << ", " << p.z[i] << ")\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_arrays<point, 2>;
2// equivalent to:
3// struct points {
4//   std::array<float, 2> x;
5//   std::array<float, 2> y;
6//   std::array<float, 2> 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; ++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 当中。

要实现这样的效果,首先需要定义 FlagsOption

 1struct Flags {
2    bool use_short;
3    bool use_long;
4};
5
6template <typename T, Flags flags>
7struct Option {
8    std::optional<T> 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 方式。argcargv 被传递进来,下一步操作:

 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_descriptiondefine_class 来完成简单类型的代码生成工作。

因为不支持 Expansion statements,因此下一步需要借助新类型 Zexpand() 来进行参数遍历。

 1template <typename Spec>
2auto Clap::parse(this Spec const& spec, int argc, char** argv) {
3    // ...
4
5    struct Z {
6        std::meta::info spec;
7        std::meta::info opt;
8    };
9
10    [:std::meta::expand([]() consteval {
11        auto spec_members = nonstatic_data_members_of(^Spec);
12        auto opts_members = nonstatic_data_members_of(^Opts);
13
14        std::vector<Z> v;
15        for (size_t i = 0; i != spec_members.size(); ++i) {
16            v.push_back({.spec=spec_members[i], .opt=opts_members[i]});
17        }
18        return v;
19    }()):] >> [&]<auto Z>{
20        // ...
21    }

Z 包含两个成员,分别保存 ArgsOpts 的非静态数据成员信息,当前示例它的大小为 2。每一组信息就对应一个参数,2 个分别对应 -n-c

如果用 Expansion statements 写,逻辑则会更加清晰,对应的写法为:

1template for (constexpr auto [sm, om] : std::views::zip(nonstatic_data_members_of(^Spec),
2                                        nonstatic_data_members_of(^Opts))) 
{
3    // ...
4}

具体处理每一组参数的逻辑如下:

 1template <typename Spec>
2auto Clap::parse(this Spec const& spec, int argc, char** argv) {
3    // ...
4    >> [&]<auto Z>{
5        constexpr auto sm = Z.spec;
6        constexpr auto om = Z.opt;
7
8        auto& cur = spec.[:sm:];
9
10        // find the argument associated with this option
11        auto it = std::find_if(cmdline.begin(), cmdline.end(),
12            [&](std::string_view arg){
13            return cur.use_short && arg.size() == 2 && arg[0] == '-' && arg[1] == name_of(sm)[0]
14                || cur.use_long && arg.starts_with("--") && arg.substr(2) == name_of(sm);
15        });
16
17
18        if (it == cmdline.end()) {
19            // no such argument
20            if constexpr (has_template_arguments(type_of(om)) && template_of(type_of(om)== ^std::optional) {
21                // the type is optional, sot he argument is too
22                return;
23            } else if (cur.initializer) {
24                // the type isn't optional, but an initializer is provided, use that
25                opts.[:om:] = *cur.initializer;
26                return;
27            } else {
28                std::cerr << "Missing required option " << name_of(sm) << '\n';
29                std::exit(EXIT_FAILURE);
30            }
31        } else if (it + 1 == cmdline.end()) {
32            std::cout << "Option " << *it << " for " << name_of(sm) << " is missing a value\n";
33            std::exit(EXIT_FAILURE);
34        }
35
36        // alright, found our argument, try to parse it
37        auto iss = std::ispanstream(it[1]);
38        if (iss >> opts.[:om:]; !iss) {
39            std::cerr << "Failed to parse " << it[1] << " into option " << name_of(sm)
40                      << " of type " << name_of(type_of(om))
41                      << '\n';
42            std::exit(EXIT_FAILURE);
43        }
44    };
45
46    return opts;
47}

整体实现思路就是根据 cur 中的信息在参数列表 cmdline 中查找,如果没有查到,则看参数是否可选,有可选默认值的,把该值读取出来,保存到 opts 中;如果查找到的位置后面没有紧跟参数值,如 -n 后面什么也没有,则缺少参数值。

如果找到了参数,则使用 C++23 std::ispanstream 将值读取到 opts 返回值当中,it 查找到的位置为参数的位置,参数位置后面 it[1] 就是参数值的位置。

如此便借助反射实现了一个可定制的 Clap,逻辑还是比较清晰的,但受限于当前的实现,绕了一些路,显得麻烦了一些。

完整实现为:

 1// The Library
2namespace clap {
3    struct Flags {
4        bool use_short;
5        bool use_long;
6    };
7
8    template <typename T, Flags flags>
9    struct Option {
10        std::optional<T> initializer;
11
12        Option() = default;
13        Option(T t) : initializer(t) { }
14
15        static constexpr bool use_short = flags.use_short;
16        static constexpr bool use_long = flags.use_long;
17    };
18
19    consteval auto spec_to_opts(std::meta::info opts, std::meta::info spec) -> std::meta::info {
20        std::vector<std::meta::nsdm_description> new_members;
21        for (auto member : nonstatic_data_members_of(spec)) {
22            auto new_type = template_arguments_of(type_of(member))[0];
23            new_members.push_back(make_nsdm_description(new_type, {.name=name_of(member)}));
24        }
25        return define_class(opts, new_members);
26    }
27
28    struct Clap {
29        template <typename Spec>
30        auto parse(this Spec const& spec, int argc, char** argv) {
31            std::vector<std::string_view> cmdline(argv + 1, argv + argc);
32
33            // check if cmdline contains --help, etc.
34
35            struct Opts;
36            static_assert(is_type(spec_to_opts(^Opts, ^Spec)));
37            Opts opts;
38
39            struct Z {
40                std::meta::info spec;
41                std::meta::info opt;
42            };
43
44            [:std::meta::expand([]() consteval {
45                auto spec_members = nonstatic_data_members_of(^Spec);
46                auto opts_members = nonstatic_data_members_of(^Opts);
47
48                std::vector<Z> v;
49                for (size_t i = 0; i != spec_members.size(); ++i) {
50                    v.push_back({.spec=spec_members[i], .opt=opts_members[i]});
51                }
52                return v;
53            }()):] >> [&]<auto Z>{
54                constexpr auto sm = Z.spec;
55                constexpr auto om = Z.opt;
56
57                auto& cur = spec.[:sm:];
58
59                // find the argument associated with this option
60                auto it = std::find_if(cmdline.begin(), cmdline.end(),
61                    [&](std::string_view arg){
62                        return cur.use_short && arg.size() == 2 && arg[0] == '-' && arg[1] == name_of(sm)[0]
63                            || cur.use_long && arg.starts_with("--") && arg.substr(2) == name_of(sm);
64                    });
65
66
67                if (it == cmdline.end()) {
68                    // no such argument
69                    if constexpr (has_template_arguments(type_of(om)) && template_of(type_of(om)== ^std::optional) {
70                        // the type is optional, sot he argument is too
71                        return;
72                    } else if (cur.initializer) {
73                        // the type isn't optional, but an initializer is provided, use that
74                        opts.[:om:] = *cur.initializer;
75                        return;
76                    } else {
77                        std::cerr << "Missing required option " << name_of(sm) << '\n';
78                        std::exit(EXIT_FAILURE);
79                    }
80                } else if (it + 1 == cmdline.end()) {
81                    std::cout << "Option " << *it << " for " << name_of(sm) << " is missing a value\n";
82                    std::exit(EXIT_FAILURE);
83                }
84
85                // alright, found our argument, try to parse it
86                auto iss = std::ispanstream(it[1]);
87                if (iss >> opts.[:om:]; !iss) {
88                    std::cerr << "Failed to parse " << it[1] << " into option " << name_of(sm)
89                            << " of type " << name_of(type_of(om))
90                            << '\n';
91                    std::exit(EXIT_FAILURE);
92                }
93            };
94
95            return opts;
96        }
97    };
98}

小结

还有许多例子,因为本文已较长,决定后面再写,否则读起来也比较累。

若按照 100% 来谈论反射的进度,前两年更新之时进度大概在 20% - 30%,而如今大概到了 30% - 40%。从本文也可以看到实现已更加完善,最新的语法也全部支持,其他相关的反射特性也有了平替的丐版实现,虽说还不够简便,也缺少很多功能,但至少能用了。

我想 C++ 反射也是要分几次标准才能真正完善,进度到 60% 大概有机会第一次进标准,也就是进 C++26。此时会缺少 Source code injection 这种强特性,以及 Custom Attributes 这类辅助特性,只会包含最基本的反射特性。

即使如此,也将敲开 C++ 第三阶段元编程的大门,绝对会是一个强大的 C++ 新纪元,产生式元编程也将更加流行。


CppMore
Dive deep into the C++ core, and discover more!
 最新文章