11.4 变参模板
在 C++11 之前,如果你想写一个像 C 语言的 printf 那样接受任意个、不同类型的参数的函数,你只能使用非常不安全的 C 风格可变参数列表(...)加上宏来解析提取。
C++11 引入了现代且类型安全的变参模板(Variadic Templates),它允许模板接受任意数量和任意类型的模板参数组合。
参数包(Parameter Pack)
Section titled “参数包(Parameter Pack)”变参模板的核心语法是引入省略号 ...(称为参数包)。你可以把它看作是一个包裹着很多个类型(或数据)的包裹。
有两处需要使用到 ... 语法结构:
typename... Args:定义一个模板参数包(包含不定个数的类型)。Args... args:由模板参数包定义一个函数参数包(代表具体的多个入参的值)。
典型的使用场景:递归解包
Section titled “典型的使用场景:递归解包”如何将我们收到的未知的 n 个参数一个个解开并使用呢?在 C++17 之前,最经典的做法是递归展开。
你需要编写两个函数:
- 一个基本情况(Base Case):当包裹里没东西(或者只有一个固定的东西)时如何退出。
- 一个变参解析模板:剥离出收到的包裹里面的第一个元素来处理,然后把剩余的包裹作为参数再次递归调用自己。
下面我们来实现一个全泛型的连环 print 打印函数:
#include <iostream>
// 1. 无参版本的递归终点:当所有的包解析完后调用void print() { std::cout << "--- 结束打印 ---\n";}
// 2. 递归解包的变参模板// 剥离出第一个参数 firstArg(类型为 T),然后把剩下的其他参数保留在 args 包中template <typename T, typename... Args>void print(T firstArg, Args... args) {
std::cout << firstArg << "\n"; // 处理第一个参数
// 把剩下的包裹自动拆开扔给下一个函数 print(args...);}
int main() { // 调用它!不论是什么类型、无论给多少个都会被递归正确消化。 print(1, 2.5, "Hello", 'A');
// 展开过程: // print(1, 2.5, "Hello", 'A') // -> cout << 1; print(2.5, "Hello", 'A') // -> cout << 2.5; print("Hello", 'A') // -> cout << "Hello"; print('A') // -> cout << 'A'; print() // -> cout << "--- 结束打印 ---" (调用终点版本)}折叠表达式(C++17特性预览)
Section titled “折叠表达式(C++17特性预览)”像上面那样写递归还是有点绕,如果需求比较简单(比如仅仅是累加所有可变参数的和),C++17 引入了超级利器:折叠表达式(Fold Expressions)。它可以直接应用运算符在参数包上进行自动解包叠加,完全抛弃递归。
#include <iostream>
template<typename... Args>auto sumAll(Args... args) { // 语法:(args + ...) 会自动将包里的数据进行:arg1 + arg2 + arg3 ... return (args + ...);}
int main() { std::cout << sumAll(1, 2, 3, 4, 5) << "\n"; // 输出 15 std::cout << sumAll(1.5, 2.5, 3.5) << "\n"; // 输出 7.5}在日常业务开发中,我们甚至很少需要由自己手写复杂的变参模板,但在阅读各种现代 C++ 开源项目的底层库(以及标准库里的 std::make_unique 和 std::make_shared 的源码)时,它是满图飞舞的基本功课。使用它编写底层基础设施代码时是非常爽快的体验。