C++ Generative Metaprogramming:
《产生式元编程》 第六章 感今朝妙艺几人知
前言
模板编程,技巧如云,妙艺似雨。第五章滔滔滚滚地讲述了模板的核心概念和常用技术,篇幅稍长,诸般妙诀,未遑悉录,故放于本章。
目录如下:
闲话不题,本章尽是一些巧妙的实践,于极尽模板产生式编程大有用处,难度不低,重要重要!
本章代码非是示例,而是 GMP 库的真实代码。
Type List
Type List 是个只装类型的容器,不含任何数据、函数或类型成员,定义相当简单。代码如下:
1template<typename...> struct type_list {};
可变模板参数是实现的核心,表示 Type List 中存入的类型。类型并无一致性要求,是以 Type List 本身还是个异构容器。为何需要这种容器?只因编译期可能需要对很多类型进行操作,借助这种容器,能够方便地保存各种各样的类型。
可是就一行代码,也没见能进行什么操作呀?其实,Type List 只是个类型容器,需要配套算法才能发挥作用,这些算法才是关键。算法可以分成如下几类:
容积(Capacity)
大小(`type_list_size_v`)
判空(`type_list_empty`)
元素访问(Element access)
索引访问(`type_list_element_t`)
前向类型(`type_list_front`)
后向类型(`type_list_back`)
尾部类型(`type_list_tail`)
内含检测(`type_list_contains_v`)
元素操作(Element manipulation)
合并(`type_list_concat_t`)
移除(`type_list_remove_t`)
前向移除(`type_list_pop_front`)
后向移除(`type_list_pop_back`)
插入(`type_list_insert_t`)
前向插入(`type_list_push_front`)
后向插入(`type_list_push_back`)
反转(`type_list_reverse_t`)
去重(`type_list_unique_t`)
过滤(`type_list_filter_t`)
搭配以上这些基础算法,Type List 方能显现妙用。以下各个小节,便来展示各个算法的实现,这些实现并不一定是最优的,但却能展示模板技巧。GMP 后续会进一步优化各个算法。
注意,GMP 命名上,以 _v
结尾表示实现为变量模板,该变量模板借助非 _v
版本实现,如 type_list_size_v
,其实是 type_list_size::value
的简写;以 _t
结尾表示实现为别名模板,该模板是非 _t
版本的简化别名,如 type_list_element_t
,其实是 type_list_element::type
的简写;无特殊标识结尾,表示实现直接借助了 _v
或 _t
版本的已有组件,如 type_list_empty
,其实是 type_list_size_v
在大小是否为 0 时的简写,不命名为 type_list_empty_v
,是因为没有相对应的 type_list_empty::value
写法。
容积(Capacity)
容积类算法,主要包含一些与大小有关的操作,Type List 不曾分配内存,大小相关的操作实际指的是与可变模板参数数量相关的操作。
大小(`type_list_size_v`)
若要获得 Type List 的大小,只需借助 sizeof...()
计算可变模板参数的个数,并非难事。
1/// class type_list_size
2template<typename> struct type_list_size;
3
4template<typename... Types>
5struct type_list_size<type_list<Types...>>
6 : std::integral_constant<std::size_t, sizeof...(Types)> {};
7
8template<typename T>
9inline constexpr std::size_t type_list_size_v = type_list_size<T>::value;
在实际开发中,为将代码重复降至最低,一般会借助 std::integral_constant
工具类,简化代码。
判空(`type_list_empty`)
判空可以直接利用 type_list_size_v
,于是实现进一步简化为:
1template<type_list_like T>
2inline constexpr bool type_list_empty = (type_list_size_v<T> == 0);
type_list_like
是检测类型是否为 Type List 的 Concept,目前组件不全,无法实现。等几个关键的算法实现完成之后,再来展示其实现。
元素访问(Element access)
元素访问类算法,主要包含类型访问的一些操作,即类型的获取操作。
索引访问(`type_list_element_t`)
索引式访问是元素访问的根本实现,只要能够按索引访问,再想实现前向访问和后向访问便是水到渠成的事。
完整实现,如下所示:
1/// class type_list_element
2template<std::size_t Idx, typename T>
3requires (Idx < type_list_size_v<T>)
4struct type_list_element;
5
6template<std::size_t Idx, typename Head, typename... Types>
7struct type_list_element<Idx, type_list<Head, Types...>>
8 : std::type_identity<typename type_list_element<Idx-1, type_list<Types...>>::type>
9{};
10
11template<typename Head, typename... Types>
12struct type_list_element<0, type_list<Head, Types...>>
13 : std::type_identity<Head>
14{};
15
16template<std::size_t Idx, typename T>
17using type_list_element_t = type_list_element<Idx, T>::type;
索引访问的核心思路,就是依次遍历,返回指定索引的类型。但模板不支持自下而上迭代,只支持自上而下递归,因此遍历与常规运行期编程差异甚大。每次递归都需要舍弃头类型,再以尾类型作为新的 Type List,直到递归终止,此时头类型就是欲访问的类型。
std::type_identity
是 C++20 加入的一个辅助模板编程的类型,用于提升抽象层次,减少代码重复。其内部不过是抽象了一个 type
类型成员而已,这个类型成员就是传入的模板参数,以是免于重复编写。再者,GMP 库基于 C++20 编写,使用 std::type_identity
并无丝毫问题。
`type_list_like` Concept
本节原本不应该出现在此处,内容不属于索引访问的子节,但又不得不置于此处,因为其实现依赖于 type_list_size_v
和 type_list_element_t
,置于前面无法写,后续组件又需用到,只剩这一处合适。
type_list_like
的作用是保证类型输入安全,倘若这些算法的类型输入不是 Type List,可能会产生未定义的行为,用户难以定位错误。需要一种方式,能够检测类型是 Type List,而 Type List 只是个带有可变模板参数的简单类型,很多用户类型都可能满足这一条件,是以检测起来并不容易。必须找出 Type List 的关键特征,区别开其他类型,才能确保类型属于 Type List。
所幸这个问题已经有人考虑过了,就是 C++23 增加的 tuple-like
Concept,用于识别 std::tuple
。照猫画虎,type_list_like
的实现为:
1/**
2 * \brief Concept to check if a type is type_list-like.
3 *
4 * \tparam T The type to be checked.
5 */
6template<typename T>
7concept type_list_like
8 = !std::is_reference_v<T> && requires(T t) {
9 typename type_list_size<T>::type;
10 requires std::derived_from<
11 type_list_size<T>,
12 std::integral_constant<std::size_t, type_list_size_v<T>>
13 >;
14 } && []<std::size_t... Is>(std::index_sequence<Is...>) {
15 return (requires(T) {
16 typename std::integral_constant<Is, std::remove_const_t<T>>;
17 } && ...);
18 }(std::make_index_sequence<type_list_size_v<T>>{});
核心涉及两个已有组件,type_list_size
和 type_list_element_t
。输入 T
若是类 Type List 类型,第一点便要满足 type_list_size<T>::type
,并且 type_list_size
继承自 std::integral_constant
;第二点稍微复杂,需要能够正常使用 std::integral_constant
访问其所含有的所有类型,借助 std::index_sequence
实现遍历。
由此,只有满足 type_list_like
约束的类型,才能作为其后所有实现算法的输入。
前向类型(`type_list_front`)
前向类型就是获得 Type List 中的第一个类型,直接借助 type_list_element_t
实现即可。
1template<type_list_like T>
2using type_list_front = type_list_element_t<0, T>;
后向类型(`type_list_back`)
后向类型就是获得 Type List 中的最后一个类型,可以直接借助 type_list_element_t
和 type_list_size_v
实现。
1template<type_list_like T>
2using type_list_back = type_list_element_t<type_list_size_v<T> - 1, T>;
尾部类型(`type_list_tail`)
尾部类型就是获取 Type List 中的非首位类型,只要移除第一个类型,剩下的即是尾部类型,因此可以直接借助后续小节实现的 type_list_pop_front
实现。
1template<type_list_like T>
2using type_list_tail = type_list_pop_front<T>;
只是一个简单的类型别名而已。
内含检测(`type_list_contains_v`)
内含检测用于检测 Type List 中是否包含指定的类型,是返回 true
,否则返回 false
。
实现为:
1/// contains
2template<typename, type_list_like>
3struct type_list_contains;
4
5template<typename U, typename... Types>
6struct type_list_contains<U, type_list<Types...>>
7 : std::bool_constant<(std::same_as<Types, U> || ...)>
8{};
9
10template<typename U, type_list_like T>
11inline constexpr bool type_list_contains_v = type_list_contains<U, T>::value;
核心逻辑就是利用 Fold Expressions 逐个对比类型,只要有一个为真,则整个条件为真,后续不会执行。
元素操作(Element manipulation)
元素访问主要是类型的获取操作,而元素操作则集中元素的修改操作。修改操作会改变 Type List,或增加类型,或删除类型。
修改操作比获取操作的实现要复杂许多。
合并(`type_list_concat_t`)
合并操作将多个 Type List 合并成一个 Type List,例如三个 Type List 分别为 type_list<int>
、type_list<double>
和 type_list<char, int>
,合并之后,产生一个新类型 type_list<int, double, char, int>
。
实现思路是将每次合并前两个 Type List,将其结果再和剩余的 Type List 合并,重复这个过程。如此一来,复杂的大问题就被拆解成为一个个小问题,降低解决难度。
代码为:
1/// class type_list_concat
2template<type_list_like TypeList1, type_list_like TypeList2, type_list_like... RestTypeLists>
3struct type_list_concat
4 : std::type_identity<typename type_list_concat<
5 typename type_list_concat<TypeList1, TypeList2>::type,
6 RestTypeLists...>::type>
7{};
8
9template<typename... LTypes, typename... RTypes>
10struct type_list_concat<type_list<LTypes...>, type_list<RTypes...>>
11 : std::type_identity<type_list<LTypes..., RTypes...>>
12{};
13
14template<type_list_like... TypeLists>
15using type_list_concat_t = type_list_concat<TypeLists...>::type;
只需专心处理合并两个 Type List 的小问题,此问题便迎刃而解。
移除(`type_list_remove_t`)
移除操作将删除 Type List 中指定位置的类型,返回移除后的 Type List。对于运行期编程来说,这种操作并不困难,而此处是编译期的类型移除,只能使用模板,颇为不易。
首先,定义好元类型形式,确定输入类型和安全约束。
1/// class type_list_remove
2template<std::size_t Idx, type_list_like T>
3requires (Idx < type_list_size_v<T>)
4struct type_list_remove;
type_list_remove
接受两个模板参数,第一个是索引值,第二个是一个 Type List,索引值必须小于其长度。
其次,确定思路,理清递归逻辑。每次递归,索引减一,取出类型列表中的头类型,将其保存到头类型列表。随着递归执行,头类型列表中的类型会依次增加,类型列表中的类型会依次减少。当递归终止,舍弃将放入头类型列表中的新类型,合并头类型列表和类型列表,便是移除之后的新 Type List。
这个逻辑需要额外的模板参数,以表示头类型列表,于是需要一个新的辅助模板 type_list_remove_impl
。
先来看 type_list_remove
的偏特化定义,核心逻辑转发至 type_list_remove_impl
实现。代码如下:
1/// class type_list_remove
2template<std::size_t Idx, typename Head, typename... Types>
3struct type_list_remove<Idx, type_list<Head, Types...>>
4 : std::type_identity<typename detail::type_list_remove_impl<Idx-1,
5 type_list<Types...>, type_list<Head>>::type>
6{};
7
8template<typename Head, typename... Types>
9struct type_list_remove<0, type_list<Head, Types...>>
10 : std::type_identity<type_list<Types...>>
11{};
当索引为 0 时,则需单独处理,否则数据将溢出,遂单独特化。
再来看 type_list_remove_impl
的具体实现:
1/// remove impl
2template<std::size_t, type_list_like, type_list_like>
3struct type_list_remove_impl;
4
5template<std::size_t Idx, typename Head, typename... Types, typename... Heads>
6struct type_list_remove_impl<Idx, type_list<Head, Types...>, type_list<Heads...>>
7 : std::type_identity<typename type_list_remove_impl<Idx-1, type_list<Types...>, type_list<Heads..., Head>>::type>
8{};
9
10template<typename Head, typename... Types, typename... Heads>
11struct type_list_remove_impl<0, type_list<Head, Types...>, type_list<Heads...>>
12 : std::type_identity<type_list_concat_t<type_list<Heads...>, type_list<Types...>>>
13{};
逻辑思路前文已交代清楚,不再絮烦。到递归终止时,舍弃此时的头类型,再使用 type_list_concat_t
合并头类型列表和随着递归不断减少的类型列表,即是移除类型之后的 Type List。
最后,别忘了设置简化别名。
1template<std::size_t Idx, type_list_like T>
2using type_list_remove_t = type_list_remove<Idx, T>::type;
type_list_remove_impl
为内部类,用户不可使用,type_list_remove
用来有些麻烦,让用户直接使用 type_list_remove_t
,以简化代码。
基于已有实现,前向移除和后向移除的实现不过是小菜一碟。代码如下:
1// removes the first type
2template<type_list_like T>
3using type_list_pop_front = type_list_remove_t<0, T>;
4
5// removes the last type
6template<type_list_like T>
7using type_list_pop_back = type_list_remove_t<type_list_size_v<T> - 1, T>;
插入(`type_list_insert_t`)
插入操作,在 Type List 的指定位置插入新的类型。
整体实现和移除操作的逻辑大体相同,分成三步走。
第一步,定义元类型形式,确定输入类型及其安全性。
1template<std::size_t Idx, typename, type_list_like T>
2requires (Idx <= type_list_size_v<T>)
3struct type_list_insert;
与移除操作不同,插入操作的索引位置可以和 Type List 的大小相等,表示在列表的末尾插入类型。
第二步,确定思路。依旧是依次取出头类型,放入头类型列表,类型列表每次只余尾类型,递归终止时,合并头类型列表、新类型和尾类型即可。当在 Type List 的开头插入类型时,单独处理,直接创建一个新的 Type List。
当然,实际参数依旧复杂,需要借助 type_list_insert_impl
辅助类。type_list_insert
本身只作简单处理,实际逻辑由辅助类完成。
type_list_insert
代码如下:
1template<std::size_t Idx, typename NewType, typename Head, typename... Types>
2requires (Idx > 0)
3struct type_list_insert<Idx, NewType, type_list<Head, Types...>>
4 : std::type_identity<typename detail::type_list_insert_impl<Idx-1, NewType, type_list<Head>, type_list<Types...>>::type>
5{};
6
7template<typename NewType, typename... Types>
8struct type_list_insert<0, NewType, type_list<Types...>>
9 : std::type_identity<type_list<NewType, Types...>>
10{};
type_list_insert_impl
是实现的核心所在,代码为:
1/// insert impl
2template<std::size_t, typename, type_list_like, type_list_like>
3struct type_list_insert_impl;
4
5template<std::size_t Idx, typename NewType, typename Head, typename... Heads, typename... Types>
6requires (Idx > 0)
7struct type_list_insert_impl<Idx, NewType, type_list<Heads...>, type_list<Head, Types...>>
8 : std::type_identity<typename type_list_insert_impl<Idx-1, NewType, type_list<Heads..., Head>, type_list<Types...>>::type>
9{};
10
11template<typename NewType, typename... Heads, typename... Types>
12struct type_list_insert_impl<0, NewType, type_list<Heads...>, type_list<Types...>>
13 : std::type_identity<type_list<Heads..., NewType, Types...>>
14{};
没甚稀奇,不另细述。
功能完成之后,再为其添加简化使用的别名模板。
1template<std::size_t Idx, typename NewType, type_list_like T>
2using type_list_insert_t = type_list_insert<Idx, NewType, T>::type;
基于此,不难实现前向插入和后向插入。代码为:
1// inserts a type to the beginning
2template<typename NewType, type_list_like T>
3using type_list_push_front = type_list_insert_t<0, NewType, T>;
4
5// adds a type to the end
6template<typename NewType, type_list_like T>
7using type_list_push_back = type_list_insert_t<type_list_size_v<T>, NewType, T>;
反转(`type_list_reverse_t`)
反转操作,就是逆序现有的 Type List 类型。比如,type_list<int, double, char>
反转后为 type_list<char, double, int>
。
原本实现这个功能比较复杂,但借助前面已经实现的组件,复杂度显著降低。
整体的实现思路,就是每次利用 type_list_back
取出 Type List 的后向类型,装入一个新的 Type List,再利用 type_list_pop_back
移除输入 Type List 的后向类型。循环往复,直到递归结束,所有类型已全归新位。
基本实现为:
1/// reverse
2template<type_list_like> struct type_list_reverse;
3
4template<typename... Types>
5struct type_list_reverse<type_list<Types...>>
6 : std::type_identity<typename detail::type_list_reverse_impl<
7 type_list_size_v<type_list<Types...>> - 1,
8 type_list<type_list_back<type_list<Types...>>>,
9 type_list_pop_back<type_list<Types...>>>::type>
10{};
11
12template<>
13struct type_list_reverse<type_list<>>
14 : std::type_identity<type_list<>>
15{};
16
17template<type_list_like T>
18using type_list_reverse_t = type_list_reverse<T>::type;
具体实现需要借助 type_list_reverse_impl
辅助类,它的实现如下:
1/// reverse impl
2template<std::size_t, type_list_like, type_list_like>
3struct type_list_reverse_impl;
4
5template<std::size_t Idx, typename... NewTypes, typename... Types>
6struct type_list_reverse_impl<Idx, type_list<NewTypes...>, type_list<Types...>>
7 : std::type_identity<typename type_list_reverse_impl<Idx-1,
8 type_list<NewTypes..., type_list_back<type_list<Types...>>>,
9 type_list_pop_back<type_list<Types...>>>::type>
10{};
11
12template<typename... NewTypes>
13struct type_list_reverse_impl<0, type_list<NewTypes...>, type_list<>>
14 : std::type_identity<type_list<NewTypes...>>
15{};
思路便是前面的思路,递归终止时,新的 Type List 长度将和输入 Type List 的长度相等,输入 Type List 的长度将变为 0。
去重(`type_list_unique_t`)
去重操作,用于移除 Type List 中的重复类型,例如,type_list<int, double, int, double, double>
去重后为 type_list<int, double>
。
这个操作比之前的所有操作都要复杂,因为它要既存在针对当前类型的操作,也存在针对过去类型的操作。每个当前对比的类型,都要和之前的类型对比,以保证不存在重复。但是,我们已经实现过内含检测操作(type_list_contains_v),它可以直接检测是否重复,只要返回为真,则重复,否则为未重复。于是,将前面用到的思路全部整合起来 ,每次取出头类型,检测其是否出现在头类型列表,若未存在,则加入头类型列表,否则遗弃。同时,Type List 类型也在每轮递归中依次减少头类型,以保证递归正常执行。
type_list_unique
的实现如下:
1/// unique
2template<type_list_like> struct type_list_unique;
3
4template<typename Head, typename... Types>
5struct type_list_unique<type_list<Head, Types...>>
6 : std::type_identity<typename detail::type_list_unique_impl<
7 type_list_size_v<type_list<Head, Types...>> - 1,
8 type_list<>, type_list<Types...>, Head, false>::type>
9{};
10
11template<>
12struct type_list_unique<type_list<>>
13 : std::type_identity<type_list<>>
14{};
15
16template<typename T>
17using type_list_unique_t = type_list_unique<T>::type;
初始化时,头类型列表为空,故为 type_list<>
,其中必然不存在当前的头类型,初始条件直接置为 false
即可。
实际工作被转移至 type_list_unique_impl
辅助类,由于需要接收条件判断的结果,如今实现都需要变成双份,一份处理 true
的情况,一份处理 false
的情况。代码为:
1/// unique impl
2template<std::size_t, type_list_like, type_list_like, typename, bool>
3struct type_list_unique_impl;
4
5template<std::size_t Idx, typename Head, typename CompType, typename... NewTypes, typename... Types>
6struct type_list_unique_impl<Idx, type_list<NewTypes...>, type_list<Head, Types...>, CompType, true>
7 : std::type_identity<typename type_list_unique_impl<Idx-1,
8 type_list<NewTypes...>, type_list<Types...>, Head,
9 type_list_contains_v<Head, type_list<NewTypes...>>>::type>
10{};
11
12template<std::size_t Idx, typename Head, typename CompType, typename... NewTypes, typename... Types>
13struct type_list_unique_impl<Idx, type_list<NewTypes...>, type_list<Head, Types...>, CompType, false>
14 : std::type_identity<typename type_list_unique_impl<Idx-1,
15 type_list<NewTypes..., CompType>, type_list<Types...>, Head,
16 type_list_contains_v<Head, type_list<NewTypes..., CompType>>>::type>
17{};
18
19template<typename CompType, typename... NewTypes, typename... Types>
20struct type_list_unique_impl<0, type_list<NewTypes...>, type_list<Types...>, CompType, true>
21 : std::type_identity<type_list<NewTypes..., Types...>>
22{};
23
24template<typename CompType, typename... NewTypes, typename... Types>
25struct type_list_unique_impl<0, type_list<NewTypes...>, type_list<Types...>, CompType, false>
26 : std::type_identity<type_list<NewTypes..., CompType, Types...>>
27{};
只要明确以上逻辑思路,这个代码理解起来并不复杂,不敢饶舌。
过滤(`type_list_filter_t`)
过滤操作,指的是根据特定条件,移除满足条件的所有元素。
如第五章所言,C++ 当前只支持三种类型的模板参数,Variable template、Concept 和 Universal 这三种类型尚不支持,因此表示条件只能借助类模板作为模板参数来实现。
声明如下:
1/// filter
2template<type_list_like, template<typename> class Pred>
3requires std::same_as<std::remove_const_t<decltype(Pred<void>::value)>, bool>
4struct type_list_filter;
5
6template<type_list_like T, template<typename> class Pred>
7using type_list_filter_t = type_list_filter<T, Pred>::type;
其中,Pred
表示条件,需要使用模板模板参数表示,可自行定义,但是这个类型必须提供一个 bool
型的 value
成员,作为条件。例如:
1template<typename T>
2using IntType = std::bool_constant<std::same_as<T, int>>;
3
4// generates `type_list<int, int>`
5type_list_filter_t<type_list<int, int, double>, IntType>
将 IntType
作为 Pred
传入,将过滤掉所有非 int
类型,只留下 int
类型。
实现思路和去重操作类似,只是加入头类型列表的判断条件由 Pred
提供,不再固定。只有满足条件,才将类型加入头类型列表,其他类型将被舍弃。
先来看 type_list_filter
的基本操作,和 type_list_unique
相似,代码为:
1template<template<typename> class Pred, typename Head, typename... Types>
2struct type_list_filter<type_list<Head, Types...>, Pred>
3 : std::type_identity<typename detail::type_list_filter_impl<
4 type_list_size_v<type_list<Head, Types...>> - 1,
5 type_list<>, type_list<Types...>, Pred, Head,
6 Pred<Head>::value>::type>
7{};
8
9template<template<typename> class Pred>
10struct type_list_filter<type_list<>, Pred>
11 : std::type_identity<type_list<>>
12{};
再来看辅助类 type_list_filter_impl
的核心实现,依旧与 type_list_unique_impl
相似,代码如下:
1template<
2 std::size_t,
3 type_list_like,
4 type_list_like,
5 template<typename> class,
6 typename, bool
7>
8struct type_list_filter_impl;
9
10template<std::size_t Idx, template<typename> class Pred, typename Head, typename CompType, typename... Heads, typename... Types>
11struct type_list_filter_impl<Idx, type_list<Heads...>, type_list<Head, Types...>, Pred, CompType, true>
12 : std::type_identity<typename type_list_filter_impl<Idx-1,
13 type_list<Heads..., CompType>, type_list<Types...>,
14 Pred, Head, Pred<Head>::value>::type>
15{};
16
17template<std::size_t Idx, template<typename> class Pred, typename Head, typename CompType, typename... Heads, typename... Types>
18struct type_list_filter_impl<Idx, type_list<Heads...>, type_list<Head, Types...>, Pred, CompType, false>
19 : std::type_identity<typename type_list_filter_impl<Idx-1,
20 type_list<Heads...>, type_list<Types...>,
21 Pred, Head, Pred<Head>::value>::type>
22{};
23
24template<template<typename> class Pred, typename CompType, typename... Heads, typename... Types>
25struct type_list_filter_impl<0, type_list<Heads...>, type_list<Types...>, Pred, CompType, true>
26 : std::type_identity<type_list<Heads..., CompType, Types...>>
27{};
28
29template<template<typename> class Pred, typename CompType, typename... Heads, typename... Types>
30struct type_list_filter_impl<0, type_list<Heads...>, type_list<Types...>, Pred, CompType, false>
31 : std::type_identity<type_list<Heads..., Types...>>
32{};
虽说实现甚为复杂,模板参数亦是不少,但有前面各种其他操作的实现过渡,循序渐进,由浅及深,已不难理解,故不再赘述。
编译期循环
屡有写到,遍历方式分为两种,自下而上叫迭代,自上而下叫递归。
模板存在多种类型,包含变量模板、函数模板和类模板,使用最多的便是递归这种遍历方式,因为目前只有递归才是函数和类共同支持的机制。若是需要操纵类型,像 Type List 的诸多算法,就只能使用递归。函数模板和变量模板都没有操纵类型的能力,它们直接返回的是一个值,而非类型,尽管可以借助 decltype(val)
从值推导出类型,却会绕个圈子,语法上也不直观,是以类模板是唯一可以使用的方式。这意味着,if constexpr
这种简化递归的方式无法使用,它们需要依赖函数使用,而类似 Type List 这种情况,不存在一个函数。
Type List 在使用时不用生成变量,只需要类型本身,但也有很多情境需要的是值,如 std::tuple
,此时就多了一种遍历——迭代。相较于递归,迭代有诸多优势,一是易于理解,二是支持随机访问,三是无须额外的终止函数或特化类。递归需要保存很多中间类型,往往需要借助辅助类才能完成略显复杂的工作,而迭代只存在于一个函数里面,不必增加模板参数来保存中间变量。
总而言之,迭代是一种更加自然的遍历方式,本节便来介绍此类技巧。
Expansion Statements(C++26 maybe)
Expansion Statements 就是 template for
,编译期的循环语法,过去几年写的反射系列文章里面已进过多次,但近月该提案终于被人重拾,发布了 P1306R2 。本节重写一下本特性,更新一下内容。
Expansion Statements 能够直接生成一些重复性的语句,而不必使用复杂的的模板实例化机制。例如,借助这种方式迭代 std::tuple
,可以这样写:
1auto tup = std::make_tuple(0, 'a', 3.14);
2template for (auto elem : tup)
3 std::cout << elem << std::endl;
template for
语句中的代码,将对 std::tuple
的每个元素展开,生成如下代码:
1auto tup = std::make_tuple(0, 'a', 3.14);
2
3{
4 auto elem = std::get<0>(tup);
5 std::cout << elem << std::endl;
6}
7
8{
9 auto elem = std::get<1>(tup);
10 std::cout << elem << std::endl;
11}
12
13{
14 auto elem = std::get<2>(tup);
15 std::cout << elem << std::endl;
16}
这种方式直接了当,语法形式和运行期的 for
循环并无太大差别,用来更加方便。但是需要区分概念,Expansion Statements 并不是循环,而是对提供的语句进行一系列的实例化展开,以达到循环的效果。变量在每次实例化都会重新展开,类型可以不同,而真正的循环不会对每个迭代元素都生成一份代码,类型是相同的。因此,它的名字叫 Expansion Statements,而不是 Compile-time for。
在新的修订版中,expansion-init-list 语法更加简洁,允许直接像下面这样写:
1template for (auto elem : {0, 'a', 3.14})
2 std::cout << elem << std::endl;
{0, 'a', 3.14}
中的大括号用于标记展开的元素范围。当前 Expansion Statements 并不能完全和运行期 for
语句对应,比如它并尚不支持 break
和 continue
操作。
Expansion Statements 允许展开的内容如下:
可析构的类(包含原始结构体和
std::tuple
)Constexpr ranges(包含编译期
std::vector
)大括号分割的表达式列表(expansion-init-list ,包含参数包展开)
此外,为了方便计数实例化展开次数,一个 enumerate
辅助功能也被添加了进来,举个例子:
1template for (auto x : enumerate(some_tuple)) {
2 // x has a count and a value
3 std::println("{}: {}", x.count, x.value);
4
5 // The count is also a compile-time constant.
6 using T = decltype(x);
7 std::array<int, T::count> a;
8}
这个辅助函数不难实现,只是简单地返回了一个元素为计数和值的元组适配器。
总体来说,Expansion Statements 通常和静态反射配套使用更有用,过去已经写过数篇文章,此处不再烦絮。
Compile-time for
Expansion Statements 对应于 Range-based for 语句,只能集中处理局部元素,还有另一种原始的 for 语句,存在索引下标,能够从整体视角下处理元素,意即不仅可以操作当前元素,还可以操作任何其他位置的元素。对于某些复杂的情况,我们需要这种能够随机访问任意位置的编译期循环方式。
举个例子,Type List 反转算法的实现,除了当前的思路,还有另一种思路,就是利用 Type List 长度减去每次迭代的索引值,得到的值就是反转之后的类型索引,再用 type_list_element_t
直接取得对应类型,存入一个新的 Type List 中便可了事。此处将以 std::tuple
作为示例,思路完全一致,不同之处在于 std::tuple
存在数据成员,一般是作为变量使用,所以不必局限于类模板,可以使用函数模板。
反转 std::tuple
的实现代码如下:
1void reverse_of(auto... args) {
2 auto tuple = std::make_tuple(std::forward<decltype(args)>(args)...);
3 return [&tuple]<auto... Is>(std::index_sequence<Is...>) {
4 return std::make_tuple(std::get<sizeof...(args) - 1 - Is>(tuple)...);
5 }(std::index_sequence_for<decltype(args)...>{});
6}
这里暴露了频繁被使用的一个迭代技巧——Compile-time for。索引通过 std::index_sequence
表示,不像运行期迭代时那样需要初始索引、终止条件和索引递增/递减,编译期索引直接包含所有索引值,可同时表示运行期迭代的这三个语句,再结合 Fold Expressions 进行实际迭代。
将语法抽象出来,便可知 Compile-time for 的基本表达形式为:
1[]<auto... I>(std::index_sequence<I...>) {}(std::make_index_sequence<N>{})
这个迭代方式很有用,比如用它来实现一个遍历 std::tuple
的函数,代码如下:
1template<typename Tuple>
2void for_each(const Tuple& tuple, auto f) {
3 [&tuple, &f]<auto... I>(std::index_sequence<I...>) {
4 (f(std::get<I>(tuple)), ...);
5 }(std::make_index_sequence<std::tuple_size_v<Tuple>>{});
6}
7
8
9int main() {
10 auto tuple = std::make_tuple(0, 'a', 3.14);
11
12 // 0 a 3.14
13 for_each(tuple, [](auto val) {
14 std::cout << val << " ";
15 });
16}
基于该技巧,灵活运用,能够实现许多强大的工具,虽然语法有些奇怪,但这其实就是目前的编译期循环语法。GMP 也会大量使用该技巧,实现不计其数的元编程组件。
递归继承
若论模板当中最复杂最精妙的技术是什么,那无疑是递归继承。该技术源自 Andrei Alexandrescu 二十多年前的著作 Modern C++ Design,随书产生的 Loki 库将模板技术运用到了极致。
递归继承就是根据可变模板参数,递归地继承类型,如上一章中展示的经典 std::tuple
实现法。此节所说的递归继承更深一层,指的是泛化的递归继承,无具体类型,复杂度飙升。
具体来说,递归继承又可分成线性递归继承和散乱递归继承。
线性递归继承
线性递归继承,继承形状犹如一串糖葫芦,从底部依次往上继承。std::tuple
的实现便是线性递归继承最简单的形式,每次继承自身,传递尾部模板参数,直到 std::tuple<>
。本节谈论的情境更加复杂,指的是泛化的线性递归继承,即允许定制的线性递归继承。
泛化是由特殊到一般的过程,是对具体情境抽象化后的结果。若 std::tuple
这般,以线性递归继承逐次往类型当中注入成员变量,只是众多产生式需求中的一种,某些需求可能想要往类型当中注入成员函数、虚函数等其他代码,不论是何种需求,都是注入代码。因此,提取注入定制点是泛化的第一步。这个提取的注入点称为 MetaFun
,表示生成代码的元函数,函数签名如下:
1template<typename T, typename Root>
2class MetaFun {};
T
表示当前的具体产品,Root
是包含剩余产品的继承体系。例如,tuple<int, double>
若采用这种泛化实现,将先继承自 MetaFun<int, tuple<double>>
,再继承自 tuple<double>
,次再继承 MetaFun<double, tuple<>>
,最后继承 tuple<>
。可见,泛化的线性递归继承是个交替继承的过程,每次通过 MetaFun
注入代码,通过自身解参递归继承。线性递归继承需要包含结束条件,表示最后继承的 Root
,某些类型可能没有特定的继承,泛化实现提供了一个空类型表示默认的结束条件,定义如下:
1/// Empty class
2struct empty_type {};
结束条件既定,定制点和继承流程亦明,于是泛化实现可以实现为:
1namespace gmp
2{
3
4/// Generates linear hierarchy
5template<typename T, template<typename, typename> class MetaFun, typename Root = empty_type>
6struct gen_linear_hierarchy : MetaFun<T, Root>
7{};
8
9template<typename... Types, template<typename, typename> class MetaFun, typename Root>
10struct gen_linear_hierarchy<type_list<Types...>, MetaFun, Root>
11 : MetaFun<type_list_front<type_list<Types...>>,
12 gen_linear_hierarchy<type_list_tail<type_list<Types...>>, MetaFun, Root>>
13{};
14
15template<typename T, template<typename, typename> class MetaFun, typename Root>
16requires std::derived_from<MetaFun<T, Root>, Root>
17struct gen_linear_hierarchy<type_list<T>, MetaFun, Root>
18 : MetaFun<T, Root>
19{};
20
21} // namespace gmp
MetaFun
作为模板参数,而其自身也是模板,由是需要使用模板模板参数。其实,在产生式元编程中,只要表示定制点,皆需使用模板模板参数,实现的功能就类似于泛化算法实现中 Lambda 表示的定制点。
线性递归继承的泛化实现并不复杂,思路却杳冥难测,流程细微巧绝,可注入定制逻辑,产生成千上万行代码。
散乱递归继承
散乱递归继承,继承形状宛如竹栅栏,由底向上,左右开枝。
这种方式以多继承消除了线性递归继承那样交替的继承体系,左边的继承表示具体产品注入代码,右边的继承表示剩余产品继承体系。因为不依赖交替继承,所以 MetaFun
也不再需要 Root
参数,剩余产品为一则是递归的结束条件。
于是实现变为:
1namespace gmp
2{
3
4/// Generates scatter hierarchy
5template<typename T, template<typename> class MetaFun>
6struct gen_scatter_hierarchy : MetaFun<T> {};
7
8template<typename... Types, template<typename> class MetaFun>
9struct gen_scatter_hierarchy<type_list<Types...>, MetaFun>
10 : gen_scatter_hierarchy<type_list_front<type_list<Types...>>, MetaFun>,
11 gen_scatter_hierarchy<type_list_tail<type_list<Types...>>, MetaFun>
12{};
13
14template<typename T, template<typename> class MetaFun>
15struct gen_scatter_hierarchy<type_list<T>, MetaFun>
16 : MetaFun<T>
17{};
18
19} // namespace gmp
实现依旧不甚复杂,复杂的是思路。每次将当前的具体产品,即 Type List 的前向类型送入递归终点,用元函数产生定制的代码,再将剩余产品(Type List 的尾部类型)传入自身进行递归继承,直到剩余产品为一,递归结束。
散乱递归继承可以和线性递归继承组合起来使用,将散乱递归继承作为线性递归继承的 Root
,当线性递归继承结束之时,便是散乱递归继承开始之时。通常,线性递归继承用于产生类型的具体接口,而散乱递归继承用于产生类型的抽象接口,两相结合,便既生成了抽象接口,又生成了具体实现。下一小节包含一个具体实例。
最佳实例:抽象工厂泛化版实现
本实例源于数年前写的设计模式系列文章中的抽象工厂一章,即 okdp 库的实现,GMP 库将旧的实现全部进行了更新,将设计模式作为一个单独的子模块提供泛型组件。本章里的递归继承是面向高级开发者的讲解,几年前写的抽象工厂文章里面含有图例讲解递归继承细节,可与本章结合阅读,降低理解难度。
抽象工厂用于产生多系列具体产品,每个产品的创建接口和具体实现一致,遂可泛化以减少代码重复。实现分为两部分,抽象实现和具体实现,抽象实现提供接口,具体实现提供真实逻辑,前者可由散乱递归继承实现,后者可由线性递归继承实现。
首先,创建抽象接口的元函数,以生成抽象实现的相关代码。如下:
1template<typename T>
2struct abstract_factory_meta_fun
3{
4 virtual T* do_create(std::type_identity<T>) = 0;
5 virtual ~abstract_factory_meta_fun() {}
6};
主要包含创建类型的 do_create()
接口,接受一个标签参数,用于在生成的众多接口中识别目标类型的准确接口。
其次,使用散乱递归继承具体实现抽象工厂的抽象部分。完整代码:
1namespace detail
2{
3
4template
5<
6 type_list_like AbstractProductList,
7 template<typename> class MetaFun = abstract_factory_meta_fun
8>
9struct abstract_factory_impl
10 : gen_scatter_hierarchy<AbstractProductList, MetaFun>
11{
12 using ProductList = AbstractProductList;
13
14 template<class T, typename... Args>
15 requires type_list_contains_v<T, AbstractProductList>
16 T* create(Args&&... args)
17 {
18 MetaFun<T>& meta = *this;
19 return meta.do_create(std::type_identity<T>{});
20 }
21
22 template<class T, typename... Args>
23 requires type_list_contains_v<T, AbstractProductList>
24 std::shared_ptr<T> create_shared(Args&&... args)
25 {
26 return std::shared_ptr<T>(create<T>(std::forward<Args>(args)...));
27 }
28
29 template<class T, typename... Args>
30 requires type_list_contains_v<T, AbstractProductList>
31 std::unique_ptr<T> create_unique(Args&&... args)
32 {
33 return std::unique_ptr<T>(create<T>(std::forward<Args>(args)...));
34 }
35};
36
37} // namespace detail
38
39
40template<typename... Types>
41using abstract_factory = detail::abstract_factory_impl<type_list<Types...>>;
每个类型都将通过散乱递归继承,以 abstract_factory_meta_fun
生成特定接口。创建类型之时,用户将调用 create()
接口,实际创建将转发调用元函数生成的 do_create()
抽象接口。create_shared()
和 create_unique()
是依赖 create()
接口创建的智能指针版本,使用起来更加方便,但需注意,欲持有已有指针,不能使用 std::make_shared
等工厂方法,而应使用原始的代理类型。
接着,创建具体实现的元函数,以生成实际创建对象的逻辑。如下:
1template<typename ConcreteProduct, typename Base>
2struct creation_meta_fun : Base
3{
4 using ProductList = type_list_tail<typename Base::ProductList>;
5
6 /*template<typename... Args>*/
7 ConcreteProduct* do_create(std::type_identity<type_list_front<typename Base::ProductList>>) override
8 {
9 return new ConcreteProduct;
10 }
11};
这个元函数主要供线性递归继承使用,故而拥有一个 Base
参数,用来交替继承。元函数中具体实现了 do_create()
接口,创建具体类型。此处的关键在于将具体实现的接口和抽象接口一一对应,每次都要通过 Type List 算法取出产品列表当中的第一个产品,以标签分发识别。ProductList
每次递归都只余尾部类型,如此递归才会终止。
最后,使用线性递归继承具体实现抽象工厂的具体部分。完整代码:
1template
2<
3 typename AbstractFactory,
4 type_list_like ConcreteProductList,
5 template<typename, typename> class Creator = creation_meta_fun
6>
7struct concrete_factory
8 : gen_linear_hierarchy<type_list_reverse_t<ConcreteProductList>, Creator, AbstractFactory>
9{};
具体工厂线性递归的终点是抽象工厂散乱递归的起点,因此第一个模板参数 AbstractFactory
就是前面实现的抽象部分。第二个模板参数是具体的产品列表,第三个是生成代码的元函数,默认设置为 creation_meta_fun
。
每次 Creator
(即 creation_meta_fun
)取出具体产品列表中的第一个产品作为 do_create()
实现的返回类型,需要和标签对应,否则类型将不一致,导致出错。但是,creation_meta_fun
从 Base::ProductList
中取出的第一个抽象产品类型的顺序和具体产品的取出顺序正好相反,于是需要把具体产品列表 ConcreteProductList
通过 Type List 反序算法调整一下顺序,才能确保正确展开对应的类型接口。
这一整个泛化过程甚为复杂,大家要记住,问题复杂性是不能消除的,只能转移。想要为一类问题抽象出一个统一解法,问题本身就是极为复杂的,但是这个复杂性可以转移到库的开发者身上,对用户隐藏。即便是以后标准支持更简单的做法,那也不过是将复杂性转移到了标准身上,对开发者隐藏。只要能够对用户隐藏复杂性,提供简单易用的接口,库这边的底层实现复杂点并不是问题。
下面是利用上述 GMP 实现,生成抽象工厂代码的应用例子。
1#include <gmp/dp/abstract_factory.hpp>
2#include <iostream>
3
4
5struct Lux {
6 virtual ~Lux() = default;
7 virtual void print() = 0;
8};
9
10struct EasyLux : Lux {
11 void print() override {
12 std::cout << "easy level lux mode\n";
13 }
14};
15
16struct HardLux : Lux {
17 void print() override {
18 std::cout << "hard level lux mode\n";
19 }
20};
21
22struct DieLux : Lux {
23 void print() override {
24 std::cout << "die level lux mode\n";
25 }
26};
27
28struct Teemo {
29 virtual ~Teemo() = default;
30 virtual void print() = 0;
31};
32
33struct EasyTeemo : Teemo {
34 void print() override {
35 std::cout << "easy level Teemo mode\n";
36 }
37};
38
39struct HardTeemo : Teemo {
40 void print() override {
41 std::cout << "hard level Teemo mode\n";
42 }
43};
44
45struct DieTeemo : Teemo {
46 void print() override {
47 std::cout << "die level Teemo mode\n";
48 }
49};
50
51struct Ziggs {
52 virtual ~Ziggs() = default;
53 virtual void print() = 0;
54};
55
56struct EasyZiggs : Ziggs {
57 void print() override {
58 std::cout << "easy level Ziggs mode\n";
59 }
60};
61
62struct HardZiggs : Ziggs {
63 void print() override {
64 std::cout << "hard level Ziggs mode\n";
65 }
66};
67
68struct DieZiggs : Ziggs {
69 void print() override {
70 std::cout << "die level Ziggs mode\n";
71 }
72};
73
74using AbstractAIFactory = gmp::abstract_factory<Ziggs, Lux, Teemo>;
75using EasyLevelAIFactory = gmp::concrete_factory<AbstractAIFactory, gmp::type_list<EasyZiggs, EasyLux, EasyTeemo>>;
76using HardLevelAIFactory = gmp::concrete_factory<AbstractAIFactory, gmp::type_list<HardZiggs, HardLux, HardTeemo>>;
77using DieLevelAIFactory = gmp::concrete_factory<AbstractAIFactory, gmp::type_list<DieZiggs, DieLux, DieTeemo>>;
78
79
80int main() {
81 auto factory = std::make_shared<DieLevelAIFactory>();
82 auto ziggs = factory->create_unique<Ziggs>();
83 auto lux = factory->create_shared<Lux>();
84 auto teemo = factory->create_shared<Teemo>();
85
86 ziggs->print();
87 lux->print();
88 teemo->print();
89}
用户无需再手动为类型重复编写抽象工厂的相关代码,直接使用 gmp::abstract_factory
和 gmp::concrete_factory
生成实际代码。
最终输出为:
1die level Ziggs mode
2die level lux mode
3die level Teemo mode
编译期获取结构体成员个数
获取结构体成员个数是反射众多功能中的一粒沙,2016 年之后,出现了 T1 级别的实现手法,无需再像 T0 级实现方式那样得手动注册或侵入。
该功能最终会产生一个值,并且依赖遍历,由是如今最简洁的实现手法是借助函数模板和 if constexpr
。经典的实现代码如下所示:
1struct Any { template <class T> operator T() const; };
2
3template <class T, class... Args>
4requires std::is_aggregate_v<T>
5consteval auto CountAggregateMembers() {
6 if constexpr (requires { T{ Args{}... }; }) {
7 return CountAggregateMembers<T, Args..., Any>();
8 } else {
9 return sizeof...(Args) - 1;
10 }
11}
12
13struct S {
14 int a;
15 double b;
16 std::string c;
17};
18
19int main() {
20 // 3
21 std::cout << CountAggregateMembers<S>();
22}
核心思路不算复杂,就是构造一个能够隐式转换为任何类型的对象逐次去初始化目标类型,起初先用一个 Any
去初始化 S
对象,之后依次增加,一旦增加的个数超过目标类型的成员个数,初始化便会失败,此即为递归终止条件。终止时,当前的 Any
个数刚好比目标类型的成员个数多一,减去便是所求的结构体成员个数。
这个实现简单,是因为既用到了 C++17 if constexpr
,又用到了 C++20 Concepts,前者极大简化了传统模板递归方式,后者极大简化了传统模板约束方式。
C++17 之前的遍历方式也可以用来解决同样的问题,但相对来说要麻烦一些。下面列举几种其他方式,改写上述代码。
第一种,借助 SFINAE,递归以传统方式实现。
1template <class T, class... Args>
2std::enable_if_t<std::is_aggregate_v<T> && !std::is_constructible_v<T, Args...>, std::size_t>
3CountAggregateMembers() {
4 return sizeof...(Args) - 1;
5}
6
7template <class T, class... Args>
8std::enable_if_t<std::is_aggregate_v<T> && std::is_constructible_v<T, Args...>, std::size_t>
9CountAggregateMembers() {
10 return CountAggregateMembers<T, Args..., Any>();
11}
传统方式实现函数模板递归,不可避免地需要多写一个函数来表示结束条件,是以表达同样的逻辑,得多写一些重复的代码。SFINAE 更不必提,本来就是偶然发现的模板约束技巧,易用性和可读性自然不比专门为模板约束而设计的 Concepts。
还有一点需要注意,std::is_constructible_v
在 C++20 之前并不支持聚合类型的构造检测,必须为类型提供构造函数。像下面这样使用将存在问题:
1struct S {
2 int a;
3 double b;
4 std::string c;
5};
6
7// Compile time error before C++20
8static_assert(std::is_constructible_v<S, Any, Any, Any>);
因此,上述 SFINAE 实现存在隐患,为解决这一问题,可以自己实现一个 is_aggregate_constructible
替换 std::is_constructible_v
。代码如下:
1template <typename T, typename... Args>
2class is_aggregate_constructible {
3private:
4 template <typename U, typename... A>
5 static auto test(int) -> decltype(U{std::declval<A>()...}, std::true_type{});
6
7 template <typename, typename...>
8 static auto test(...) -> std::false_type;
9
10public:
11 static constexpr bool value = decltype(test<T, Args...>(0))::value;
12};
13
14template <typename T, typename... Args>
15inline constexpr bool is_aggregate_constructible_v = is_aggregate_constructible<T, Args...>::value;
利用这个工具,便可保证检测无误。
1// OK
2static_assert(is_aggregate_constructible_v<S, Any, Any, Any>);
第二种,借助 Compile-time for,以迭代替换递归完成遍历。
1template <class T, std::size_t N>
2concept ConstructibleWithN = requires {
3 []<std::size_t... I>(std::index_sequence<I...>) -> decltype(T{ (I, Any{})... }) {
4 return {};
5 }(std::make_index_sequence<N>{});
6};
7
8template <class T, std::size_t N>
9concept CanAggregate = std::is_aggregate_v<T> && ConstructibleWithN<T, N> && !ConstructibleWithN<T, N + 1>;
10
11template <class T>
12constexpr auto CountAggregateMembers = []<std::size_t... I>(std::index_sequence<I...>) {
13 std::size_t R;
14 ((CanAggregate<T, I> ? (R = I, true) : false) || ...);
15 return R;
16}(std::make_index_sequence<64>{});
逻辑思路不变,只是改变了遍历方式。实现变得更加复杂,主要是因为 Compile-time for 的索引在编译期就已生成,而当前问题并无法明确索引范围,只能预先提供一个相对较大的索引范围,并且要借助 Fold expressions 的一些高级技巧来展开逻辑。仅是提供一种思路,此问题的解决方式其实更适合自上而下的递归。
第三种,Tag dispatching,这种方式也是利用函数重载。
1template<typename T, typename... Args>
2std::size_t CountAggregateMembersImpl(std::false_type) {
3 return sizeof...(Args) - 1;
4}
5
6template<typename T, typename... Args>
7std::size_t CountAggregateMembersImpl(std::true_type) {
8 return CountAggregateMembersImpl<T, Args..., Any>(
9 std::integral_constant<bool,
10 std::is_aggregate_v<T> && is_aggregate_constructible_v<T, Args..., Any>>{}
11 );
12}
13
14template<typename T>
15std::size_t CountAggregateMembers() {
16 return CountAggregateMembersImpl<T>(std::true_type{});
17}
逻辑与第一种方式完全一致,只是借助标签分发不同逻辑而已。看看即可,已不推荐此种方式。
编译期获取结构体成员名称
获取结构体成员名称是反射中的另一个小小元函数,以前只有 T0 级的实现方式,需要手动注册成员信息。二三年冬十二月,有人分享了一种 T1 级的实现技巧,不几日,reflect-cpp 等反射库皆用新的方式替换了 T0 级手法。
简化的实现,代码如下:
1template<class T> extern const T external;
2
3template<auto Ptr>
4consteval auto name_of() -> std::string_view
5{
6 const auto name = std::string_view{std::source_location::current().function_name()};
7#if defined(__clang__)
8 const auto split = name.substr(0, name.find("}]"));
9 return split.substr(split.find_last_of(".") + 1);
10#elif defined(__GNUC__)
11 const auto split = name.substr(0, name.find(");"));
12 return split.substr(split.find_last_of(":") + 1);
13#elif defined(_MSC_VER)
14 const auto split = name.substr(0, name.find_last_of("}"));
15 return split.substr(split.find_last_of(">") + 1);
16#endif
17}
18
19template<std::size_t Idx, typename T>
20consteval auto get(T&& t)
21{
22 auto&& [a, b] = t;
23
24 if constexpr (Idx == 0) return &a;
25 else return &b;
26}
27
28template<std::size_t Idx, typename T>
29consteval auto member_name() -> std::string_view
30{
31 constexpr auto name = name_of<get<Idx>(external<T>)>();
32 return name;
33}
借助 member_name
,可以获取指定位置的结构体成员名称。示例:
1struct S {
2 int foo;
3 char bar;
4};
5
6int main() {
7 // Output: bar
8 std::cout << gmp::member_name<1, S>();
9}
该实现技巧的核心并不出奇,早在二二年秋的文章「如何优雅地打印类型名称」中便讲到此法,就是通过编译器扩展结合 std::string_view
手动解析类型名称。奇妙之处,在于如何触发出包含成员名称的信息?此法巧妙地结合了 magic_get
的实现技巧——利用 Structure Bindings 获取指定位置的结构体成员值,再通过 NTTP 将获取到的结构体成员地址作为指针传入函数模板,从而使函数名称当中包含成员变量的名称信息。此外,另一个技巧是借助 extern
声明一个外部变量模板,这个场景只需在编译期使用 external<T>
产生类型信息,并不在运行期使用该变量,而外部变量无需定义,可以避免实例化产生过多的模板变量。
需要注意,示例中的实现并不完善,get
尚只支持固定数量的结构体,但扩展起来并非难事,只需借助上一节编译期获取结构体成员个数所实现的工具,假设工具名为 member_count
,那么实现可以像下面这样扩展:
1template<std::size_t Idx, typename T>
2consteval auto get(T&& t)
3{
4 if constexpr (member_count<T>() == 1) {
5 auto&& [p1] = t;
6 if constexpr (Idx == 0) return &p1;
7 } else if (member_count<T>() == 2) {
8 auto&& [p1, p2] = t;
9 if constexpr (Idx == 0) return &p1;
10 if constexpr (Idx == 1) return &p2;
11 }
12 // ...
13}
这招也是穷举法,重复写到一定数量,能够满足日常需求即可。只有等 C++26 Pack structure bindings 和 Pack indexing 真正可用后,才能从根本上解决该问题,到那时,以上代码便可以简化为:
1template<std::size_t Idx, typename T>
2consteval auto get(T&& t)
3{
4 // pack structure bindings
5 auto&& [...elems] = t;
6
7 // pack indexing
8 return &elems...[Idx];
9}
这才是直击要害的解决方式,技巧到底不过是扬汤止沸之法。只是等到 C++26,反射大抵已入标准,这个场景下已没必要再自行实现获取成员名称了。
反射在本系列下篇还有专门的章节讲述,本章只是涉及其中的两个元函数而已,实现并不完善,先尝尝鲜罢。
编译期消息分发
本节内容是 2023 年写过的一篇文章,展示了一种编译期消息分发技术。
写过的内容不会原封不动地搬过来,若是忘记内容可移步重读。这种实现技术里面暴露了一些非常有用的技术,一是字符串参数,二是强制编译期执行。对于前者,在第五章专门谈过模板参数,NTTP 并不直接支持字符串字面量,必须借助一个封装的类使用,这个封装的 string_literal
极其有用,它使得能够直接以 NTTP 的形式传递字符串字面量,允许在编译期使用字符串,对于某些需求来说非常必要。对于后者,后续章节会具体解释,本节不题。
附上当初的最终实现与示例:
1template <std::size_t N>
2struct string_literal {
3 constexpr string_literal(char const (&str)[N]) {
4 std::copy_n(str, N, value);
5 }
6
7 friend constexpr bool operator==(string_literal const& s, char const* cause) {
8 return std::string_view(s.value) == cause;
9 }
10
11 constexpr operator char const*() const {
12 return value;
13 }
14
15 char value[N];
16};
17
18// default implementation
19template <string_literal C>
20inline constexpr auto handler = [] { std::cout << "default effect\n"; };
21
22#define _(name) template<> inline constexpr auto handler<#name>
23
24// opt-in customization points
25_(cause 1) = [] { std::cout << "customization points effect 1\n"; };
26_(cause 2) = [] { std::cout << "customization points effect 2\n"; };
27
28template <auto> struct compile_time_param {};
29template <string_literal Data> inline auto compile_time_arg = compile_time_param<Data>{};
30
31template <string_literal... Cs>
32struct dispatcher {
33 template <string_literal C>
34 constexpr auto execute_if(char const* cause) const {
35 return C == cause ? handler<C>(), true : false;
36 }
37
38 // compile-time and run time interface, O(n)
39 constexpr auto execute(char const* cause) const {
40 (!execute_if<Cs>(cause) && ...);
41 }
42
43 // compile-time interface, O(1)
44 template<string_literal s>
45 consteval auto execute(compile_time_param<s>) const {
46 handler<s>();
47 }
48};
49
50_(cause 4) = [] { /* compile time statements*/ };
51_(cause 5) = [] { /* compile time statements*/ };
52
53int main() {
54 constexpr string_literal cause_1{ "cause 1" };
55 constexpr dispatcher<cause_1, "cause 2", "cause 3"> dispatch;
56 dispatch.execute(cause_1); // customization points effect 1
57 dispatch.execute("cause 2"); // customization points effect 2
58 dispatch.execute("cause 3"); // default effect
59
60 constexpr string_literal cause_4{ "cause 4" };
61 const char cause_5[] = "cause 5";
62
63 dispatch.execute(compile_time_arg<cause_4>); // OK
64 dispatch.execute(compile_time_arg<"cause 5">); // OK
65 // dispatch.execute(compile_time_arg<cause_5>); // Error
66}
总结
本章与第五章相辅相成,全面回顾了模板的理论和诸多妙技。篇幅有限,简单技术并未单独讨论,重心皆在高级技术上面。
妙艺有新有旧,实现却悉为新的手法,去除了过去的一些组件,转而利用标准已有组件,减少了代码重复。
任何产生式技术,不论是宏还是模板,遍历都是核心技术,也是本章讲解的核心,递归和迭代,皆有优劣及使用场景。递归继承是模板代码生成的巅峰,是最复杂也是最精妙的技术,线性递归继承和散乱递归继承,一个实现逻辑,一个实现接口,一个是具体实现,一个是抽象表达,组合起来,能够实现复杂的代码注入功能。编译期获取结构体成员个数和名称是静态反射中的两个小小元函数,现有技术早能实现,只是仍旧复杂一些,代码重复难以尽除,这是底层特性的缺失,非应用层实现所能根除。编译期消息分发是字符串 NTTP 的一个巧绝应用,展示了一些关键技术,后续依旧有用。
总而言之,模板的顶端技术,本章几近一网全收,可反复阅读。