浅析C++可变参数模板的展开方式
前言
可变参数模板(variadic templates)是C++11新增的强大的特性之一,它对模板参数进行了高度泛化,能表示0到任意个数、任意类型的参数。相比C++98/03这些类模版和函数模版中只能含固定数量模版参数的“老古董”,可变模版参数无疑是一个巨大的进步。
如果是刚接触可变参数模板可能会觉得比较抽象,使用起来会不太顺手,使用可变参数模板时通常离不开模板参数的展开,所以本文来列举一些常用的模板展开方式,帮助我们来对可变参数模板有一个初步的了解。
可变参数模板的定义
可变参数模板和普通模板的定义类似,在写法上需要在 typename
或 class
后面带上省略号...
,以下为一个常见的可变参数函数模板:
template <class... T> void func(T... args) { //... }
上面这个函数模板的参数 args
前面有省略号,所以它就是一个被称为模板参数包(template parameter pack)的可变模版参数,它里面包含了0到N个模版参数,而我们是无法直接获取 args
中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这也是本文要重点总结的内容。
参数包的展开
参数包展开的方式随着c++语言的发展也在与时俱进,我们以实现一个可变参格式化打印函数为例,列举一些常用的方式:
递归函数方式展开
#include <iostream> void FormatPrint() { std::cout << std::endl; } template <class T, class ...Args> void FormatPrint(T first, Args... args) { std::cout << "[" << first << "]"; FormatPrint(args...); } int main(void) { FormatPrint(1, 2, 3, 4); FormatPrint("good", 2, "hello", 4, 110); return 0; }
这种递归展开的方式与递归函数的定义是一样的,需要递归出口和不断调用自身,仔细看看这个函数模板是不是都满足啦?递归出口就是这个无模板参数的 FormatPrint
,并且在有参模板中一直在调用自身,递归调用的过程时这样的 FormatPrint(4,3,2,1)
-> FormatPrint(3,2,1)
-> FormatPrint(2,1)
-> FormatPrint(1)
-> FormatPrint()
,输出内容如下:
>albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++11
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
[1][2][3][4]
[good][2][hello][4][110]
逗号表达式展开
#include <iostream> template <class ...Args> void FormatPrint(Args... args) { (void)std::initializer_list<int>{ (std::cout << "[" << args << "]", 0)... }; std::cout << std::endl; } int main(void) { FormatPrint(1, 2, 3, 4); FormatPrint("good", 2, "hello", 4, 110); return 0; }
这种方式用到了C++11的新特性初始化列表(Initializer lists)以及很传统的逗号表达式,我们知道逗号表达式的优先级最低,(a, b)
这个表达式的值就是 b
,那么上述代码中(std::cout << "[" << args << "]", 0)
这个表达式的值就是0,初始化列表保证其中的内容从左往右执行,args参数包会被逐步展开,表达式前的(void)
是为了防止变量未使用的警告,运行过后我们就得到了一个N个元素为0的初始化列表,内容也被格式化输出了:
albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++11
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
[1][2][3][4]
[good][2][hello][4][110]
说到这顺便提一下,可以使用sizeof...(args)
得到参数包中参数个数。
enable_if方式展开
#include <iostream> #include <tuple> #include <type_traits> template<std::size_t k = 0, typename tup> typename std::enable_if<k == std::tuple_size<tup>::value>::type FormatTuple(const tup& t) { std::cout << std::endl; } template<std::size_t k = 0, typename tup> typename std::enable_if<k < std::tuple_size<tup>::value>::type FormatTuple(const tup& t){ std::cout << "[" << std::get<k>(t) << "]"; FormatTuple<k + 1>(t); } template<typename... Args> void FormatPrint(Args... args) { FormatTuple(std::make_tuple(args...)); } int main(void) { FormatPrint(1, 2, 3, 4); FormatPrint("good", 2, "hello", 4, 110); return 0; }
C++11的enable_if
常用于构建需要根据不同的类型的条件实例化不同模板的时候。顾名思义,当满足条件时类型有效。可作为选择类型的小工具,其广泛的应用在 C++ 的模板元编程(meta programming)之中,利用的就是SFINAE原则,英文全称为Substitution failure is not an error,意思就是匹配失败不是错误,假如有一个特化会导致编译时错误,只要还有别的选择,那么就无视这个特化错误而去选择另外的实现,这里的特化概念不再展开,感兴趣可以自行了解,后续可以单独总结一下。
在上面的代码实现中,基本思路是先将可变模版参数转换为std::tuple
,然后通过递增参数的索引来选择恰当的FormatTuple
函数,当参数的索引小于tuple元素个数时,会不断取出当前索引位置的参数并输出,当参数索引等于总的参数个数时调用另一个模板重载函数终止递归,编译运行输入以下内容:
albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++11
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
[1][2][3][4]
[good][2][hello][4][110]
折叠表达式展开(c++17)
#include <iostream> template<typename... Args> void FormatPrint(Args... args) { (std::cout << ... << args) << std::endl; } int main(void) { FormatPrint(1, 2, 3, 4); FormatPrint("good", 2, "hello", 4, 110); return 0; }
折叠表达式(Fold Expressions)是C++17新引进的语法特性,使用折叠表达式可以简化对C++11中引入的参数包的处理,可以在某些情况下避免使用递归,更加方便的展开参数,如上述代码中展示的这样可以方便的展开参数包,不过输出的内容和之前的有些不一样:
albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++17
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
1234
good2hello4110
对比结果发现缺少了格式化的信息,需要以辅助函数的方式来格式化:
#include <iostream> template<typename T> string format(T t) { std::stringstream ss; ss << "[" << t << "]"; return ss.str(); } template<typename... Args> void FormatPrint(Args... args) { (std::cout << ... << format(args)) << std::endl; } int main(void) { FormatPrint(1, 2, 3, 4); FormatPrint("good", 2, "hello", 4, 110); return 0; }
这次格式化内容就被加进来了:
albert@home-pc:/mnt/d/data/cpp/testtemplate$ g++ testtemplate.cpp --std=c++17
albert@home-pc:/mnt/d/data/cpp/testtemplate$ ./a.out
[1][2][3][4]
[good][2][hello][4][110]
这样好像还是有点麻烦,我们可以把折叠表达式和逗号表达式组合使用,这样得到的代码就简单多啦,也能完成格式化输出的任务:
#include <iostream> template<typename... Args> void FormatPrint(Args... args) { (std::cout << ... << (std::cout << "[" << args, "]")) << std::endl; } int main(void) { FormatPrint(1, 2, 3, 4); FormatPrint("good", 2, "hello", 4, 110); return 0; }
总结
Variadic templates
是C++11新增的强大的特性之一,它对模板参数进行了高度泛化Initializer lists
是C++11新加的特性,可以作为函数参数和返回值,长度不受限制比较方便Fold Expressions
是C++17新引进的语法特性,可以方便的展开可变参数模板的参数包可变参数模板的参数包在C++11的环境下,可以利用递归、逗号表达式、enable_if等方式进行展开
==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==