目录一、为什么需要可变参数模板二、基本语法参数包Parameter Packsizeof... 运算符三、参数包展开方式1递归展开最直观方式2初始化列表展开C11更高效四、折叠表达式C17 简化五、完美转发与可变参数模板六、tuple 的实现原理简化版 tuple 实现七、完整例子类型安全的 printf八、常见错误1. 忘记递归终止函数2. 在运行时使用 sizeof...3. 参数包展开顺序错误九、这一篇的收获一、为什么需要可变参数模板在 C98/03 中实现一个任意数量参数的函数很麻烦cpp// 方式1重载最多 N 个 void print() {} void print(int a) {} void print(int a, int b) {} void print(int a, int b, int c) {} // 永远不够... // 方式2使用 va_list类型不安全 #include cstdarg void badPrint(const char* fmt, ...) { va_list args; va_start(args, fmt); // 容易出错没有类型检查 vprintf(fmt, args); va_end(args); } badPrint(%d %s %f, 42, hello, 3.14); // 格式字符串必须匹配可变参数模板类型安全、编译期检查、无限参数。cpptemplatetypename... Args void printAll(Args... args) { // 可以处理任意数量的参数每个都有自己的类型 } printAll(1, hello, 3.14, c); // 编译期展开二、基本语法参数包Parameter Packcpp// Args 是类型参数包 templatetypename... Args class Tuple {}; // args 是函数参数包 templatetypename... Args void func(Args... args) {} // 使用 Tupleint, double, string t; // Args {int, double, string} func(1, 2.5, hello); // args {1, 2.5, hello}sizeof... 运算符获取参数包中的参数个数编译期常量cpptemplatetypename... Args void countArgs(Args... args) { cout 类型数量: sizeof...(Args) endl; cout 参数数量: sizeof...(args) endl; } countArgs(1, hello, 3.14); // 类型数量: 3参数数量: 3 countArgs(); // 类型数量: 0参数数量: 0三、参数包展开参数包不能直接使用需要通过展开获取每个元素。主要有两种展开方式。方式1递归展开最直观cpp// 终止函数空参数时调用 void print() { cout endl; } // 递归函数处理第一个参数然后递归处理剩余 templatetypename T, typename... Rest void print(T first, Rest... rest) { cout first ; // 处理第一个 print(rest...); // 递归处理剩余 } int main() { print(1, 2.5, hello, c); // 输出: 1 2.5 hello c }展开过程textprint(1, 2.5, hello, c) → cout 1; print(2.5, hello, c) → cout 2.5; print(hello, c) → cout hello; print(c) → cout c; print() → 换行方式2初始化列表展开C11更高效利用数组初始化列表的求值顺序保证从左到右一次性展开所有参数cpptemplatetypename... Args void printAll(Args... args) { // 使用 int 数组的初始化列表来触发展开 int dummy[] { (cout args , 0)... }; // 或者更优雅的方式 // (cout ... args); // C17 折叠表达式后面会讲 } printAll(1, 2.5, hello); // 输出: 1 2.5 hello解释(cout args , 0)是一个逗号表达式输出args然后返回0{ (expr)... }将expr对每个args展开生成{0, 0, 0}数组数组不需要使用只是为了触发展开四、折叠表达式C17 简化C17 引入了折叠表达式让参数包展开更加简洁cpp// 一元右折叠 templatetypename... Args void printAll(Args... args) { (cout ... args) endl; // ((cout arg1) arg2) ... } // 带分隔符 templatetypename... Args void printWithComma(Args... args) { ((cout args (sizeof...(args) 1 ? : , )), ...); cout endl; } // 求和 templatetypename... Args auto sum(Args... args) { return (args ...); // 右折叠arg1 (arg2 (arg3 ...)) } int main() { printAll(1, 2.5, hello); // 1 2.5 hello cout sum(1, 2, 3, 4) endl; // 10 }折叠类型语法展开结果一元右折叠(args ...)(arg1 (arg2 (arg3 ...)))一元左折叠(... args)(((arg1 arg2) arg3) ...)二元折叠(args ... init)带初始值五、完美转发与可变参数模板在库开发中常常需要将参数包完美转发给另一个函数cpptemplatetypename T, typename... Args unique_ptrT make_unique(Args... args) { // 完美转发每个参数 return unique_ptrT(new T(forwardArgs(args)...)); } // 使用 auto p make_uniquestring(10, a); // 等价于 new string(10, a)展开过程forwardArgs(args)...展开为forwardArg1(arg1), forwardArg2(arg2), ...六、tuple 的实现原理std::tuple是可变参数模板的经典应用可以存储任意多个不同类型的值。简化版 tuple 实现cpp#include iostream #include typeinfo using namespace std; // 前向声明 templatetypename... Types class Tuple; // 空 tuple 的特化终止条件 template class Tuple {}; // 递归定义一个头部 尾部 templatetypename Head, typename... Tail class TupleHead, Tail... : private TupleTail... { private: Head value; public: Tuple() : value(), TupleTail...() {} Tuple(const Head h, const Tail... t) : value(h), TupleTail...(t...) {} // 获取头部 Head head() { return value; } const Head head() const { return value; } // 获取尾部 TupleTail... tail() { return *this; } const TupleTail... tail() const { return *this; } }; // 获取 tuple 中第 N 个元素编译期递归 templatesize_t N, typename Tuple struct TupleElement; templatetypename Head, typename... Tail struct TupleElement0, TupleHead, Tail... { using type Head; static Head get(TupleHead, Tail... t) { return t.head(); } }; templatesize_t N, typename Head, typename... Tail struct TupleElementN, TupleHead, Tail... { using type typename TupleElementN-1, TupleTail...::type; static type get(TupleHead, Tail... t) { return TupleElementN-1, TupleTail...::get(t.tail()); } }; // get 函数模板 templatesize_t N, typename... Types auto get(TupleTypes... t) { return TupleElementN, TupleTypes...::get(t); } // 打印 tuple递归展开 templatesize_t N, typename... Types void printTuple(const TupleTypes... t) { if constexpr (N sizeof...(Types)) { cout getN(t); if constexpr (N 1 sizeof...(Types)) { cout , ; } printTupleN1(t); } } templatetypename... Types void print(const TupleTypes... t) { cout (; printTuple0(t); cout ) endl; } int main() { Tupleint, double, string t(42, 3.14, hello); cout get0(t) endl; // 42 cout get1(t) endl; // 3.14 cout get2(t) endl; // hello print(t); // (42, 3.14, hello) return 0; }原理TupleA, B, C继承自TupleB, C再继承自TupleC再继承自Tuple每个层级存储一个元素valuegetN通过模板递归找到第 N 层的元素七、完整例子类型安全的 printfcpp#include iostream #include string using namespace std; // 基础情况没有参数时只输出格式字符串中的普通字符 void formatPrint(const char* fmt) { while (*fmt) { if (*fmt % *(fmt 1) %) { fmt; } cout *fmt; fmt; } } // 递归处理匹配 % 并输出对应参数 templatetypename T, typename... Args void formatPrint(const char* fmt, T value, Args... args) { while (*fmt) { if (*fmt %) { fmt; if (*fmt %) { cout %; fmt; continue; } // 输出当前参数 cout value; // 递归处理剩余参数 formatPrint(fmt 1, args...); return; } cout *fmt; fmt; } } // 辅助宏或函数 templatetypename... Args void myPrintf(const char* fmt, Args... args) { formatPrint(fmt, args...); } int main() { myPrintf(Hello, %!\n, world); myPrintf(% % %\n, 1, 2, 3); myPrintf(Percent sign: %%\n); myPrintf(Mixed: % is %.2f, and %\n, 42, 3.14159, done); return 0; }输出textHello, world! 1 2 3 Percent sign: % Mixed: 42 is 3.14, and done八、常见错误1. 忘记递归终止函数cpp// ❌ 缺少空参数版本递归不会终止 templatetypename T, typename... Rest void print(T first, Rest... rest) { cout first; print(rest...); // 当 rest 为空时找不到匹配 }2. 在运行时使用 sizeof...cppint n sizeof...(args); // ✅ 编译期常量 // 但不能这样 if (sizeof...(args) 0) { ... } // ✅ 可以编译期判断3. 参数包展开顺序错误cpp// 初始化列表展开的顺序是确定的从左到右 int arr[] { (cout args, 0)... }; // 顺序正确 // 但函数参数的求值顺序不确定 func(func1(args...), func2(args...)); // 危险九、这一篇的收获你现在应该理解可变参数模板templatetypename... Args接受任意数量和类型的参数sizeof...获取参数包大小编译期常量递归展开用终止函数 递归模板处理参数包初始化列表展开利用{ (expr, 0)... }一次性展开折叠表达式C17(args ...)简化展开tuple原理递归继承 特化实现getN编译期索引 小作业实现一个makeArray函数接受任意多个参数返回std::array需要 C17。要求auto arr makeArray(1, 2, 3, 4);得到arrayint, 4{1,2,3,4}。提示需要推导数组大小。下一篇预告第44篇《typename与class的区别依赖类型名与template消除歧义》——模板中typename和class大多数时候可以互换但有种情况必须用typename告诉编译器“这是个类型”。template关键字也有类似的消歧义作用。下篇讲清楚这些细节。