跳转至

可变参数模板学习小记

详细的讲解请看 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>,而此时的 ValueTypechar,正是我们想要的索引为 2 的那个元素的类型。

同时,Tuple<char> 也是 Tuple<int, float, char> 的基类,因此要通过强制类型转换把 Tuple<int, float, char> 转换成 Tuple<char>,这么做是为了一层一层地舍弃当前元组的 head 而保留基类元组的 head,实现了递归展开的效果。(具体分析和设计思路请看 里缪的文章