当然,变长模板也不是百利而无一害的,首先变长模板的参数包始终可以解包出空包,这会导致模板的偏特化和主模板发生歧义,因此在处理一些元函数(编译期计算出某些元数据的模板类就叫做元函数,概念来自于boost.mpl)的时候就要格外小心;其次,虽然我们方便了类型定义和部分的处理,但是向list头部添加数据就很困难了,参考下面的例子:
// TL1是一个包含int和long的list,现在我们在头部添加一个short // 古典实现很简单 using New = TypeList<short, TL1>; // 而现代的实现就没那么轻松了 // using New = TypeList<short, TL1>; 这么做是错的问题出在哪?...运算符只能对参数包进行解包扩展,而TL1是一个类型,不是参数包,但是我们有需要把TL1包含的参数拿出来,于是问题就出现了。
对于这种需求我们只能使用一个元函数来解决,这是现代化方法为数不多的缺憾之一。
元函数的实现定义了TypeList,接下来是定义各种元函数了。
也许你会疑惑为什么不把元函数定义为模板类的内部静态constexpr函数呢?现代c++不是已经具备强大的编译期计算能力了吗?
答案是否定的,编译期函数只能计算数值常量,而我们的元数据还包括了type,这时函数处理不了的。
不过话也不能说死,因为在处理数值常量的地方constexpr的作用还是很大的,后面我也会用constexpr函数辅助元函数。
Length元函数求list长度最常见的需求就是求出TypeList中存放了多少个元素,当然这也是实现起来最简单的需求。
先来看看古典技法,所谓古典技法就是让模板递归特化,依靠偏特化和特化来确定退出条件达到求值的目的。
因为编译期很难存储下迭代需要的中间状态,因此我们不得不依赖这种像递归函数般的处理技巧:
template <typename TList> struct Length; // 主模板,为下面的偏特化服务 template <> struct Length<TypeList<>> { static constexpr int value = 0; } template <typename Head, typename... Types> struct Length<TypeList<Head, Types...>> { static constexpr int value = Length<Types...>::value + 1; };解释一下,static constexpr int value是c++17的新特性,这种变量将会被视为类内的静态inline变量,可以就地初始化(c++11)。否则你可能需要将值定义为匿名的enum,这也是常见的元编程技巧之一。
我们从参数包的第一个参数开始逐个处理,遇到空包就返回0结束递归,然后从底层逐步返回,每一层都让结果+1,因为每一层代表了有一个type。
其实我们可以用c++11的新特性——sizeof...操作符,它可以直接返回参数包中参数的个数:
template <typename... Types> struct Length<TypeList<Types...>> { static constexpr int value = sizeof...(Types); };使用现代c++的代码简单明了,因为参数包总是可以展开为空包,这时候value为0,还可以少写一个特化。
TypeAt获取索引位置上的类型list上第二个常见的操作就是通过index获取对应位置的数据。为了和c++的使用习惯相同,我们规定TypeList的索引也是从0开始。
在Python中你可以这样引用list的数据list_1[3],但是我们并不会给元容器创建实体,元容器和元函数都是配合typedef或其他编译期手段实现编译期计算的,只需要用到它的类型本身和类型别名。因此我们只能这样操作元容器:using res = typename TypeAt<TList, 3>::type。
有了元函数的调用形式,我们可以开始着手实现了:
template <typename TList, unsigned int index> struct TypeAt; template <typename Head, typename... Args> struct TypeAt<TypeList<Head, Args...>, 0> { using type = Head; }; template <typename Head, typename... Args, unsigned int i> struct TypeAt<TypeList<Head, Args...>, i> { static_assert(i < sizeof...(Args) + 1, "i out of range"); using type = typename TypeAt<TypeList<Args...>, i - 1>::type; };首先还是声明主模板,具体的实现交给偏特化。
虽然c++已经支持编译期在constexpr函数中进行迭代操作了,但是对于模板参数包我们至今不能实现直接的迭代,即使是c++17提供的折叠表达式也只是实现了参数包在表达式中的就地展开,远远达不到迭代的需要。因此我们不得不用老办法,从第一个参数开始,逐渐减少参数包中参数的数量,在减少了index个后这次偏特化的模板中,index一定是0, 而Head就一定是我们需要的类型,将它设置为type即可,而上层的元函数只需要不断减少index的值,并把Head从参数包中去除,将剩下的参数和index传递给下一层的元函数TypeAt即可。