今天我们讲一个特殊的专题如何使用可变模板和 tuple 来完成一些常见的功能尤其是编译期计算。可变模板可变模板是 C11 引入的一项新功能使我们可以在模板参数里表达不定个数和类型的参数。从实际的角度它有两个明显的用途用于在通用工具模板中转发参数到另外一个函数用于在递归的模板中表达通用的情况另外会有至少一个模板特化来表达边界情况我们下面就来分开讨论一下。转发用法以标准库里的 make_unique 为例它的定义差不多是下面这个样子template typename T, typename... Args inline unique_ptrT make_unique(Args... args) { return unique_ptrT( new T(forwardArgs(args)...)); }这样它就可以把传递给自己的全部参数转发到模板参数类的构造函数上去。注意在这种情况下我们通常会使用 std::forward确保参数转发时仍然保持正确的左值或右值引用类型。稍微解释一下上面三处出现的 ...typename... Args 声明了一系列的类型——class... 或 typename... 表示后面的标识符代表了一系列的类型。Args... args 声明了一系列的形参 args其类型是 Args。forward(args)... 会在编译时实际逐项展开 Args 和 args 参数有多少项展开后就是多少项。举一个例子如果我们需要在堆上传递一个 vector假设我们希望初始构造的大小为 100每个元素都是 1那我们可以这样写make_unique(100, 1)模板实例化之后会得到相当于下面的代码template inline unique_ptrvectorint make_unique(int arg1, int arg2) { return unique_ptrvectorint( new vectorint( forwardint(arg1), forwardint(arg2))); }如前所述forward(args)... 为每一项可变模板参数都以同样的形式展开。项数也允许为零那样我们在调用构造函数时也同样没有任何参数。递归用法我们也可以用可变模板来实现编译期递归。下面就是个小例子template typename T constexpr auto sum(T x) { return x; } template typename T1, typename T2, typename... Targ constexpr auto sum(T1 x, T2 y, Targ... args) { return sum(x y, args...); }在上面的定义里如果 sum 得到的参数只有一个会走到上面那个重载。如果有两个或更多参数编译器就会选择下面那个重载执行一次加法随后你的参数数量就少了一个因而递归总会终止到上面那个重载结束计算。要使用上面这个模板我们就可以写出像下面这样的函数调用auto result sum(1, 2, 3.5, x);模板会这样依次展开sum(1 2, 3.5, x) sum(3 3.5, x) sum(6.5 x) 6.5 x注意我们都不必使用相同的数据类型只要这些数据之间可以应用 它们的类型无关紧要……再看另一个复杂些的例子函数的组合 [2]。如果我们有函数 f 和 函数 g要得到函数的联用 g∘f其满足我们能不能用一种非常简单的方式写不包含变量 x 的表达式来表示函数组合呢答案是肯定的。跟上面类似我们需要写出递归的终结情况单个函数的“组合”template typename F auto compose(F f) { return [f](auto... x) { return f( forwarddecltype(x)(x)...); }; }上面我们仅返回一个泛型 lambda 表达式保证参数可以转发到 f。记得我们在[第 16 讲] 讲过泛型 lambda 表达式本质上就是一个模板所以我们按转发用法的可变模板来理解上面的 ... 部分就对了。下面是正常有组合的情况template typename F, typename... Args auto compose(F f, Args... other) { return [f, other...](auto... x) { return f(compose(other...)( forwarddecltype(x)(x)...)); }; }在这个模板里我们返回一个 lambda 表达式然后用 f 捕捉第一个函数对象用 args... 捕捉后面的函数对象。我们用 args... 继续组合后面的部分然后把结果传到 f 里面。上面的模板定义我实际上已经有所简化没有保持值类别。完整的包含完美转发的版本请看参考资料 [3] 中的 functional.h 实现。下面我们来试验一下使用这个 compose 函数。我们先写一个对输入范围中每一项都进行平方的函数对象auto square_list [](auto container) { return fmap( [](int x) { return x * x; }, container); };我们使用了fmap而不是标准库里的 transform是因为后者接口非函数式无法组合——它要求参数给出输出位置的迭代器会修改迭代器指向的内容返回结果也只是单个的迭代器函数式的接口则期望不修改参数的内容结果完全在返回值中。我们这儿用了泛型 lambda 表达式是因为组合的时候不能使用模板只能是函数对象或函数指针——如果我们定义一个 square_list 模板的话组合时还得显式实例化才行写成 square_list 的样子很不方便。我们再写一个求和的函数对象auto sum_list [](auto container) { return accumulate( container.begin(), container.end(), 0); };那先平方再求和就可以这样简单定义了auto squared_sum compose(sum_list, square_list);我们可以验证这个定义是可以工作的vector v{1, 2, 3, 4, 5}; cout squared_sum(v) endl;我们会得到55tuple上面的写法虽然看起来还不错但实际上有个缺陷被 compose 的函数除了第一个最右边的其他的函数只能接收一个参数。要想进一步推进类似的技巧我们得首先解决这个问题。在 C 里要通用地用一个变量来表达多个值那就得看多元组——tuple 模板了。tuple 算是 C98 里的 pair 类型的一般化可以表达任意多个固定数量、固定类型的值的组合。下面这段代码约略地展示了其基本用法#include algorithm #include iostream #include string #include tuple #include vector using namespace std; // 整数、字符串、字符串的三元组 using num_tuple tupleint, string, string; ostream operator(ostream os, const num_tuple value) { os get0(value) , get1(value) , get2(value); return os; } int main() { // 阿拉伯数字、英文、法文 vectornum_tuple vn{ {1, one, un}, {2, two, deux}, {3, three, trois}, {4, four, quatre}}; // 修改第 0 项的法文 get2(vn[0]) une; // 按法文进行排序 sort(vn.begin(), vn.end(), [](auto x, auto y) { return get2(x) get2(y); }); // 输出内容 for (auto value : vn) { cout value endl; } // 输出多元组项数 constexpr auto size \ tuple_size_vnum_tuple; cout Tuple size is size endl; }输出是我们可以看到tuple 的成员数量由尖括号里写的类型数量决定。可以使用 get 函数对 tuple 的内容进行读和写。当一个类型在 tuple 中出现正好一次时我们也可以传类型取内容即对我们上面的三元组get 是合法的get 则不是。可以用 tuple_size_v 在编译期取得多元组里面的项数。如果我们要用一个三项的 tuple 去调用一个函数我们可以写类似这样的代码template class F, class Tuple constexpr decltype(auto) apply( F f, Tuple t) { return f( get0(forwardTuple(t)), get1(forwardTuple(t)), get2(forwardTuple(t))); }这似乎已经挺接近我们需要的形式了但实际调用函数的参数项数会变啊……我们已经有了参数的项数使用 tuple_size_v所以我们下面要做的是生成从 0 到项数减一之间的整数序列。标准库里已经定义了相关的工具我们需要的就是其中的 make_index_sequence其简化实现如下所示template class T, T... Ints struct integer_sequence {}; template size_t... Ints using index_sequence integer_sequencesize_t, Ints...; template size_t N, size_t... Ints struct index_sequence_helper { typedef typename index_sequence_helper N - 1, N - 1, Ints...::type type; }; template size_t... Ints struct index_sequence_helper 0, Ints... { typedef index_sequenceInts... type; }; template size_t N using make_index_sequence typename index_sequence_helper N::type;正如一般的模板代码它看起来还是有点绕的。其要点是如果我们给出 make_index_sequence则结果是 integer_sequence一下子想不清楚的话可以拿纸笔来模拟一下模板的展开过程。而有了这样一个模板的帮助之后我们就可以写出下面这样的函数同样这是标准库里的 apply 函数模板的简化版本template class F, class Tuple, size_t... I constexpr decltype(auto) apply_impl(F f, Tuple t, index_sequenceI...) { return f( getI(forwardTuple(t))...); } template class F, class Tuple constexpr decltype(auto) apply(F f, Tuple t) { return apply_impl( forwardF(f), forwardTuple(t), make_index_sequence tuple_size_v remove_reference_t Tuple{}); }我们如果有一个三元组 t类型为 tuple去 apply 到一个函数 f展开后我们得到 apply_impl(f, t, index_sequence0, 1, 2{})再展开后我们就得到了上面那个有 get0、get1、get2 的函数调用形式。换句话说我们利用一个计数序列的类型可以在编译时展开 tuple 里的各个成员并用来调用函数。数值预算上面的代码有点复杂而且似乎并没有完成什么很重要的功能。我们下面看一个源自实际项目的例子。需求是我们希望快速地计算一串二进制数中 1 比特的数量。举个例子如果我们有十进制的 31 和 254转换成二进制是 00011111 和 11111110那我们应该得到 5 7 12。显然每个数字临时去数肯定会慢我们应该预先把每个字节的 256 种情况记录下来。因而如何得到这些计数值是个问题。在没有编译期编程时我们似乎只能用另外一个程序先行计算然后把结果填进去——这就很不方便很不灵活了。有了编译期编程我们就不用写死而让编译器在编译时帮我们计算数值。利用 constexpr 函数我们计算单个数值完全没有问题。快速定义如下constexpr int count_bits(unsigned char value) { if (value 0) { return 0; } else { return (value 1) count_bits(value 1); } }可 256 个总不见得把计算语句写上 256 遍吧这就需要用到我们上面讲到的 index_sequence 了。我们定义一个模板它的参数是一个序列在初始化时这个模板会对参数里的每一项计算比特数并放到数组成员里。template size_t... V struct bit_count_t { unsigned char count[sizeof...(V)] { static_castunsigned char( count_bits(V))...}; };注意上面用 sizeof...(V) 可以获得参数的个数在 tuple_size_v 的实现里实际也用到它了。如果我们模板参数传 0, 1, 2, 3结果里面就会有个含 4 项元素的数组数值分别是对 0、1、2、3 的比特计数。然后我们当然就可以利用 make_index_sequence 来展开计算了想产生几项就可以产生几项。不过要注意到 make_index_sequence 的结果是个类型不能直接用在 bit_count_t 的构造中。我们需要用模板匹配来中转一下template size_t... V constexpr bit_count_tV... get_bit_count(index_sequenceV...) { return bit_count_tV...(); } auto bit_count get_bit_count( make_index_sequence256());得到 bit_count 后我们要计算一个序列里的比特数就只是轻松查表相加了此处不再赘述。内容小结今天我们讨论了在编译期处理不确定数量的参数和类型的基本语言特性可变模板以及可以操控可变模板的重要工具——tuple 和 index_sequence。用好这些工具可以让我们轻松地完成一些编译期计算的工作。