顺带一提,static_assert不是必须的,因为你传递了不合法的索引,编译器会直接检测出来,但是在我这(g++ 8.3, clang++ 8.0.1, vs2017)编译器对此类问题发出的抱怨实在是难以让人类去阅读,所以我们使用static_assert来明确报错信息,而其余的信息比如不合法的index是多少,编译器会给你提示。
如果你不想越界报错而是返回NullType,那么可以这样写:
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> { // 如果i越界就返回NullType using type = typename TypeAt<TypeList<Args...>, i - 1>::type; }; // 越界后的退出条件 template <unsigned int i> struct TypeAt<TypeList<>, i> { using type = NullType; };因为不想越界后报错,所以我们要提供越界之后参数包为空的退出条件,在参数包处理完后就会立即使用这个新的特化,返回NullType。
聪明的读者也许会问为什么不用SFINAE,没错,在类模板和它的偏特化中我们也可以在模板参数列表或是类名后的参数列表中使用enable_if实现SFINAE,但是这里存在两个问题,一是类名后的参数列表必须要能推演出模板参数列表里的所有项,二是类名后的参数列不能和其他偏特化相同,同时也要符合主模板的调用方式。有了如上限制,利用SFINAE就变得无比困难了。(当然如果你能找到利用SFINAE的实现,也可以通过回复告诉我,大家可以相互学习;不清楚SFINAE是什么的读者,可以参阅cppreference上的简介,非常的通俗易懂)
当然这么做的话静态断言就要被忍痛割爱了,为了接口表现的丰富性,Loki的作者将不报错的TypeAt单独实现为了不同的元函数:
template <typename TList, unsigned int Index> struct TypeAtNonStrict; template <typename Head, typename... Args> struct TypeAtNonStrict<TypeList<Head, Args...>, 0> { using type = Head; }; template <typename Head, typename... Args, unsigned int i> struct TypeAtNonStrict<TypeList<Head, Args...>, i> { using type = typename TypeAtNonStrict<TypeList<Args...>, i - 1>::type; }; template <unsigned int i> struct TypeAtNonStrict<TypeList<>, i> { using type = Null; }; IndexOf获得指定类型在list中的索引IndexOf的套路和TypeAt差不多,只不过这里的递归不用扫描整个参数包(逐个按顺序处理参数包,是不是和扫描一样呢),只需要匹配到Head和待匹配类型相同,就返回0;如果不匹配就像TypeAt中那样递归调用元函数,对其返回结果+1,因为结果在本层之后,所以需要把本层加进索引里,递归调用返回后逐渐向前相加最终的结果就是类型所在的index(从0开始)。
IndexOf一个重要的功能就是判断某个类型是否在TypeList中。
如果处理完参数包仍然找不到对应类型呢?这时候对空的TypeList做个特化返回-1就行,当然前面的偏特化元函数也需要对这种情况做处理。
现在我们来看下IndexOf的调用形式:“IndexOf<TList, int>::value”
现在我们就照着这个形式实现它:
template <typename TList, typename T> struct IndexOf; template <typename Head, typename... Tails, typename T> struct IndexOf<TypeList<Head, Tails...>, T> { private: // 为了避免表达式过长,先将递归的结果起了别名 using Result = IndexOf<TypeList<Tails...>, T>; public: // 如果类型相同就返回,否则检查递归结果,-1说明查找失败,否则返回递归结果+1 static constexpr int value = std::is_same_v<Head, T> ? 0 : (Result::value == -1 ? -1 : Result::value + 1); }; // 终止条件,没找到对应类型 template <typename T> struct IndexOf<TypeList<>, T> { static constexpr int value = -1; };因为有了c++11的type_traits的帮助,我们可以偷懒少写了一个类似这样的偏特化:
template <typename... Tails, typename T> struct IndexOf<TypeList<T, Tails...>, T> { static constexpr int value = 0; };然而现代c++的威力远不止如此,前面我们说过不能对参数包实现迭代,但是我们可以借助折叠表达式、constexpr函数,编译期容器这三者,将参数包中每一个参数映射到编译期容器中,之后便可以对编译期容器进行迭代操作,避免了递归偏特化。
当然,这种方案只是证明了c++的可能性,真正实现起来比递归的方式要麻烦的多,性能可能也并不会比递归好多少(当然都是编译期的计算,不会付出运行时代价),而且需要一个完全支持c++14,至少支持c++17折叠表达式的编译期(vs2019可以设置使用clang,原生的编译器对c++17的支持有点惨不忍睹)。
技术的关键是c++14的std::array和std::index_sequence。