可变模板参数是 C++11 引入的重要特性,允许模板接受任意数量、任意类型的参数,极大地增强了模板的灵活性和表达能力。
下面通过代码示例从基础到进阶逐步讲解。
#include <iostream> // 声明一个可变参数的函数模板 // Args 是一个模板参数包,可以包含零个或多个类型 template<typename... Args> void print(Args... args) { // args 是一个函数参数包,包含零个或多个实际参数值 std::cout << "参数数量: " << sizeof...(args) << std::endl; } int main() { print(); // 参数数量: 0 print(1); // 参数数量: 1 print(1, 3.14, "hello"); // 参数数量: 3 return 0; }
typename... Args:Args 称为模板参数包,可以匹配任意多个类型。
Args... args:args 称为函数参数包,用于承载实际的参数值。
sizeof...(args):编译期获取参数包中的元素个数。
要逐个处理参数包中的每个元素,通常采用递归 + 特化的方式。
// 递归终止函数(无参数版本) void print_all() { std::cout << std::endl; // 换行 } // 可变参数模板:处理第一个参数 + 剩余参数 template<typename T, typename... Rest> void print_all(T first, Rest... rest) { std::cout << first << " "; // 处理当前参数 print_all(rest...); // 递归调用,展开剩余参数 } int main() { print_all(1, 3.14, "hello", 'c'); // 输出:1 3.14 hello c return 0; }
执行过程:
print_all(1, 3.14, "hello", 'c') → 打印 1,调用 print_all(3.14, "hello", 'c')
print_all(3.14, "hello", 'c') → 打印 3.14,调用 print_all("hello", 'c')
print_all("hello", 'c') → 打印 hello,调用 print_all('c')
print_all('c') → 匹配 T=char, Rest=空,打印 c,调用 print_all()(终止函数)
print_all() 换行结束。
C++11 中可以利用初始化列表和逗号运算符在一条语句中展开参数包,避免递归。
#include <iostream> template<typename... Args> void print_all(Args... args) { // 使用一个 int 数组的初始化列表来触发包展开 int dummy[] = { (std::cout << args << " ", 0)... }; // 展开后相当于: // { (cout << 1 << " ", 0), (cout << 3.14 << " ", 0), (cout << "hello" << " ", 0) }; // 逗号运算符返回 0,所以数组元素全是 0 (void)dummy; // 避免编译器未使用变量的警告 std::cout << std::endl; } int main() { print_all(1, 3.14, "hello"); return 0; }
更简洁的 C++17 写法(折叠表达式):
template<typename... Args> void print_all(Args... args) { (std::cout << ... << args) << std::endl; // 二元左折叠 }
在实际库实现中(如 std::make_unique, std::thread 构造函数),往往需要将参数完美转发给其他函数。
#include <iostream> #include <utility> // 目标函数:接收一个左值或右值 void target(int&& x) { std::cout << "右值: " << x << std::endl; } void target(int& x) { std::cout << "左值: " << x << std::endl; } // 包装器:完美转发可变参数 template<typename Func, typename... Args> void wrapper(Func&& f, Args&&... args) { // 将参数包完美转发给函数 f f(std::forward<Args>(args)...); } int main() { int a = 10; wrapper(target, a); // 左值 wrapper(target, 20); // 右值 return 0; }
Args&&... args:万能引用参数包。
std::forward<Args>(args)...:对每个参数独立执行完美转发。
std::tuple 就是利用可变模板参数存储任意多个不同类型的值。下面是一个极简实现:
#include <iostream> // 前向声明 template<typename... Types> class Tuple; // 特化:空元组 template<> class Tuple<> {}; // 递归定义:一个头部 + 尾部元组 template<typename Head, typename... Tail> class Tuple<Head, Tail...> : public Tuple<Tail...> { private: Head value; public: Tuple(Head h, Tail... t) : Tuple<Tail...>(t...), value(h) {} Head& head() { return value; } const Head& head() const { return value; } // 继承 Tuple<Tail...>,可以访问尾部 Tuple<Tail...>& tail() { return *this; } const Tuple<Tail...>& tail() const { return *this; } }; // 辅助函数:类似 std::get<0>(tuple) template<size_t N, typename Head, typename... Tail> auto& get(Tuple<Head, Tail...>& t) { if constexpr (N == 0) return t.head(); else return get<N-1>(t.tail()); } int main() { Tuple<int, double, std::string> t(42, 3.14, "hello"); std::cout << get<0>(t) << ", " << get<1>(t) << ", " << get<2>(t) << std::endl; return 0; }
sizeof... 运算符可以在编译期获得参数包中的元素个数,常用于静态断言或分配数组长度。
template<typename... Args> void check_size(Args... args) { static_assert(sizeof...(args) > 0, "至少需要一个参数"); // ... }
利用可变模板参数实现一个类似 printf 但类型安全的函数:
#include <iostream> #include <string> void safe_printf(const char* fmt) { // 无参数时,直接打印格式字符串(要求 fmt 中没有 %) while (*fmt) { if (*fmt == '%' && *(fmt+1) == '%') ++fmt; // 跳过 %% else if (*fmt == '%') throw std::runtime_error("参数不足"); else std::cout << *fmt++; } } template<typename T, typename... Args> void safe_printf(const char* fmt, T value, Args... args) { while (*fmt) { if (*fmt == '%' && *(fmt+1) == '%') { ++fmt; std::cout << '%'; } else if (*fmt == '%') { std::cout << value; // 打印当前参数 safe_printf(fmt+1, args...); // 递归处理剩余参数 return; } else { std::cout << *fmt; } ++fmt; } // 如果格式字符串结束但还有参数未使用,可以报错或忽略 } int main() { safe_printf("整数:%,浮点数:%,字符串:%\n", 42, 3.14, std::string("hello")); return 0; }
包展开的位置:... 必须紧跟在包含参数包的表达式之后,例如 args...、std::forward<Args>(args)...。
递归深度:递归展开会导致模板实例化深度等于参数个数,编译器通常支持数百层,足够日常使用。
C++17 折叠表达式:大大简化了二元运算符的参数包处理,如 (args + ...) 等。
性能:可变参数模板完全在编译期展开,没有运行时开销(除非递归函数未被内联)。
C++11 的可变模板参数通过参数包和包展开机制,实现了对任意数量、任意类型参数的泛型处理。
它被广泛应用于标准库中:std::tuple、std::function、std::make_unique、std::thread、std::async 等。
掌握它能够编写出高度灵活且类型安全的泛型代码。