可变参数模板学习小记
详细的讲解请看 CppMore 里缪大佬的 这篇文章。
此处学习一下其中的使用 递归继承 技巧实现简易元组类这个例子。
直接 "Show me the code!"
#include <iostream>
/*
先明晰省略运算符的意思:
typename... T --> 把一堆类型折叠到 T 中
T ... --> 从 T 中展开之前折叠的变量
*/
// #1: 先是 Tuple 类
// 主模板
template <typename... Types>
class Tuple;
// 全特化:作为终止递归的条件
template <>
class Tuple<> {};
template <typename Head, typename... Tail> // 将传递进来的参数分为第 1 个(称之为 Head)和其余个(第 2~N 个,折叠在 Tail 里)
class Tuple<Head, Tail...> : public Tuple<Tail...> { // 递归继承(采用公有继承)
public:
Tuple() {}
Tuple(Head v, Tail... vtails) : Tuple<Tail...>(vtails...), head_(v) {}
// 用来返回 Tuple 类的内部成员 head_ 的值,也是这个 Tuple 的第一个值
Head &head() { return head_; }
protected:
// head_ : 第一个值的意思,比如作为 Tuple<int, float, char> t(5, 2.7, 'b') 其中的 5
Head head_;
};
// #2: 然后是 TupleAt 类
// 主模板
template <std::size_t I, typename... TList>
struct TupleAt;
// 暂称 Tuple<T, TList...> 叫做“当前元组”
// 设计 TupleAt 的目的是获取到指定索引(I)处的元素的类型(称之为ValueType)(不必获取到具体的值,这个任务交给 Tuple 类的 head() 方法解决)
// ValueType 是“尾随元组”(意思是当前元组把第一项元素去掉,剩余的元素构成的元组)的第一个元素的类型
// TupleType 是“尾随元组”这一整体类型(它是当前元组的父类,因为 Tuple<Head, Tail...> 公有继承了 Tuple<Tail...>)
template <std::size_t I, typename T, typename... TList>
struct TupleAt<I, Tuple<T, TList...>> {
using ValueType = typename TupleAt<I - 1, Tuple<TList...>>::ValueType;
using TupleType = typename TupleAt<I - 1, Tuple<TList...>>::TupleType;
};
// 作为终止递归的条件:I = 0
template <typename T, typename... TList>
struct TupleAt<0, Tuple<T, TList...>> {
using ValueType = T;
using TupleType = Tuple<T, TList...>;
};
// #3: 最后还剩个 TupleGet 函数模板
/* TupleGet<I>(tuple) 这个函数模板在使用的时候(比如 TupleGet<2>(t))直观上接受两个参数:
其一,是模板参数 std::size_t I,表示索引;
其二,是函数参数 Tuple<TList...>& tuple,表示目标元组
TList 是目标元组 tuple 的模板参数们
*/
template <std::size_t I, typename... TList>
typename TupleAt<I, Tuple<TList...>>::ValueType &
TupleGet(Tuple<TList...> &tuple) {
using BaseTupleType = typename TupleAt<I, Tuple<TList...>>::TupleType;
return static_cast<BaseTupleType &>(tuple).head(); // 把当前元组强制转换其类型为上一层元组(也就是当前元组的尾随元组),.head() 的结果就会变成尾随元组的 head_
}
int
main() {
Tuple<int, float, char> t(1, 2.7, 'b');
Tuple<int, Tuple<int, float, char>, double> t2(4, t, 4.7);
std::cout << TupleGet<2>(t) << std::endl;
std::cout << TupleGet<0>(t2) << std::endl;
std::cout << TupleGet<2>(t2) << std::endl;
std::cout << TupleGet<2>(TupleGet<1>(t2)) << std::endl; // 嵌套元组也能正常处理哦
return 0;
}
这个程序实现了一个极简元组类,可以把不同类型的元素绑定在一起,并按下表索引访问元素。
每处所起到的作用已包含在注释里。这里举一个例子,简单看一下效果。
例如对于 Tuple<int, float, char> t(1, 2.7, 'b')
,在 TupleGet<2>(t)
时会发生什么。预期的结果应该是返回 char
类型的 'b'
。
TupleGet
的递归过程可以用下面这张图表示:
强制转换
TupleAt<2, <int, float, char>>::TupleType <-----+
^~~ ^~~~~~~~~~~ | |
T TList | |
| (t)
----------------+
|
v
TupleAt<1, <float, char>>::TupleType
|
|
----------------+
|
v
TupleAt<0, <char>>::TupleType
|
|
-----------+
|
v
Tuple<char>
首先在第 71 行处,t
会被强制类型转换成 TupleAt<2, <int, float, char>>::TupleType
类型。那么这一长串类型到底是什么呢?
由第 48 行可以知道,它会变成 TupleAt<1, <float, char>>::TupleType
,这一所谓 “变成” 实际上是通过将传递进来的参数分为第 1 个和其余个(第 2~N 个)。这样每次传递进来的参数会依次减少,达到遍历所有参数的效果(引自 里缪的文章)。递归的每一层,都只保留了 TList
,舍弃了 T
。
同理,TupleAt<1, <float, char>>::TupleType
会变成 TupleAt<0, <char>>::TupleType
,最终通过 I = 0
的偏特化版本变成 Tuple<char>
,而此时的 ValueType
是 char
,正是我们想要的索引为 2 的那个元素的类型。
同时,Tuple<char>
也是 Tuple<int, float, char>
的基类,因此要通过强制类型转换把 Tuple<int, float, char>
转换成 Tuple<char>
,这么做是为了一层一层地舍弃当前元组的 head
而保留基类元组的 head
,实现了递归展开的效果。(具体分析和设计思路请看 里缪的文章)