可变参数模板
1、基本介绍
C++11的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。本章我们只介绍一些基础的可变参数模板特性。
下面就是一个基本可变参数的函数模板
// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template <class ...Args> void ShowList(Args... args) {}
在不定参数的模板函数中,可以通过如下方式获得args的参数个数:
sizeof...(args)
上面的参数Args
前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了**0到N(N>=0)**个模版参数。我们无法直接获取参数包args
中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。
2、递归函数方式展开参数包
// 递归终止函数 template <class T> void ShowList(const T& t) { cout << t << endl; } // 展开函数 template <class T, class ...Args> void ShowList(T value, Args... args) { cout << value << " "; ShowList(args...); } int main() { ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort")); return 0; }
解释:
- 在执行ShowList(1)时由于存在两个重载的模板函数,在进行函数匹配时ShowList(1)会选择最匹配的
ShowList(const T& t)
,于是执行函数并进行类型推导打印出1
。 - 在执行ShowList(1, ‘A’)时,进行函数匹配时ShowList(1, ‘A’)会选择最匹配的
ShowList(T value, Args... args)
,于是执行函数并进行类型推导先打印出1
,此时参数包args
里面还有一个参数,执行下一句代码ShowList(args…),于是再次进行函数匹配于是匹配到了ShowList(const T& t)
,于是执行函数并进行类型推导打印出A
。 - 在执行ShowList(1, ‘A’, std::string(“sort”));时,进行函数匹配时ShowList(1, ‘A’, std::string(“sort”));会选择最匹配的
ShowList(T value, Args... args)
,于是执行函数并进行类型推导先打印出1
,此时参数包args
里面还有两个参数,执行下一句代码ShowList(args…),于是再次进行函数匹配于是匹配到了ShowList(T value, Args... args)
,于是执行函数并进行类型推导打印出A
,此时参数包args
里面还有一个参数,执行下一句代码ShowList(args…),于是再次进行函数匹配于是匹配到了ShowList(const T& t)
,于是执行函数并进行类型推导打印出sort
。
总结
- 可以看出第一个只有一个参数的模板函数是每一个同名的可变参数模板函数最后执行的函数,没有此函数,可变参数模板函数的递归终止条件就不能确定,所以又叫这种函数为递归终止函数。
- 可以看出可变参数模板函数,每次递归都只取出参数包里面的一个参数,经过不断递归,最终将所有的参数解析完成,故又将其称为展开函数。
在有些场景下面我们也想要一个可变参数模板函数能接收0
个参数,我们上面的写法只能接收最低1
个参数,因为我们的递归终止函数调用时就是只有1
个参数时,为了支持0个参数我们还可以这样进行编码:
// 递归终止函数 void _ShowList() { cout << endl; } // 展开函数 template <class T, class ...Args> void _ShowList(T value, Args... args) { cout << value << " "; _ShowList(args...); }
模板参数包也可以接收0个参数哦!
同样的为了让可变参数模板对外声明的接口更加统一,我们还可以包装一下展开函数与递归终止函数
// 包装一下展开函数与递归终止函数 template <class... Args> void ShowList(Args... args) { _ShowList(args...); }
// 没有包装前可变参数模板的对外声明 template <class T, class ...Args> void _ShowList(T value, Args... args); // 包装后可变参数模板的对外声明 template <class... Args> void ShowList(Args... args)
3、逗号表达式展开参数包
递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须有一个重载的递归终止函数,即必须有一个同名的终止函数来终止递归,但是这样会让人感觉稍有不便。
// 处理每个参数 template <class T> void PrintArg(T t) { cout << t << " "; } // 展开函数 template <class ...Args> void ShowList(Args... args) { int arr[] = { (PrintArg(args), 0)... }; cout << endl; } int main() { ShowList(1); ShowList(1, 'A'); ShowList(1, 'A', std::string("sort")); return 0; }
逗号表达式这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, PrintArg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand函数中的逗号表达式:(printarg(args), 0)...
,意味着将参数包进行逐个展开,变为了((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… )。
(printarg(args...), 0) // 这种写法意味着将参数包进行整体传递,不进行展开。
也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}
将会展开成最终会创建一个元素值都为0
的数组int arr[sizeof...(Args)]
。
由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)
打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。