C++模板参数包与可变参数让我们可以编写接收任意数量、任意类型参数的泛型代码,突破了传统模板只能固定参数数量的限制,是编写通用组件的重要基础。

模板参数包的基础定义
模板参数包分为两种类型,一种是类型参数包,用于接收任意数量的类型;另一种是值参数包,用于接收任意数量的编译期常量值。定义时需要在参数名前加上省略号<...>。
类型参数包的声明方式如下:
// 类型参数包T,可以接收0个或多个类型
template <typename... T>
struct TypePack {};
// 带固定参数和类型参数包的函数模板
template <typename... Args>
void func(Args... args) {}
值参数包的声明需要指定参数的类型,示例如下:
// 值参数包,接收多个int类型的编译期常量
template <int... Nums>
struct ValuePack {};
// 接收多个double类型的值参数
template <double... Values>
void print_values(Values... vals) {}
可变参数的展开方法
可变参数不能直接使用,需要通过展开操作才能访问每个参数,常见的展开方式有递归展开和折叠表达式两种。
递归展开可变参数
递归展开是最基础的方式,通过递归调用逐步处理每个参数,直到参数包为空。需要定义递归终止函数和递归函数两个版本。
#include <iostream>
// 递归终止函数,处理0个参数的情况
void print_args() {
std::cout << "参数展开完成" << std::endl;
}
// 递归函数模板,处理至少一个参数的情况
template <typename T, typename... Args>
void print_args(T first, Args... rest) {
std::cout << first << " ";
// 递归展开剩余的参数包
print_args(rest...);
}
int main() {
print_args(1, 3.14, "hello", 'a');
return 0;
}
上述代码中,每次调用print_args都会取出参数包的第一个参数处理,然后剩余参数继续递归,直到参数包为空时调用终止函数。
折叠表达式展开(C++17及以上)
C++17引入了折叠表达式,可以更简洁地展开参数包,不需要编写递归终止函数,语法更清晰。
#include <iostream>
// 折叠表达式展开参数包,计算所有参数的和
template <typename... Args>
auto sum(Args... args) {
// 二元左折叠,展开为 ((args + ...) 即 args1 + args2 + ... + argsN
return (args + ...);
}
// 打印所有参数,用逗号分隔
template <typename... Args>
void print_all(Args... args) {
// 逗号运算符折叠,展开为 (std::cout << args << ",", ...)
((std::cout << args << ", "), ...) << std::endl;
}
int main() {
std::cout << "总和:" << sum(1, 2, 3, 4) << std::endl;
print_all("apple", 100, 3.14);
return 0;
}
可变参数在函数模板中的应用
可变参数函数模板最常见的应用场景是编写通用的日志打印、参数转发等功能,下面以日志打印为例演示用法。
#include <iostream>
#include <chrono>
#include <iomanip>
// 获取当前时间的字符串
std::string get_current_time() {
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
std::string time_str = std::ctime(&time);
// 去掉末尾的换行符
time_str.pop_back();
return time_str;
}
// 通用日志打印函数,支持任意数量和类型的参数
template <typename... Args>
void log_info(Args... args) {
std::cout << "[" << get_current_time() << "] [INFO] ";
((std::cout << args), ...);
std::cout << std::endl;
}
int main() {
log_info("用户登录成功,用户ID:", 1001, ",登录IP:", "192.168.0.1");
log_info("系统启动完成,耗时:", 2.5, "秒");
return 0;
}
可变参数在类模板中的应用
类模板也可以使用可变参数,典型场景是元组std::tuple的实现,下面我们实现一个简化版的元组类。
#include <iostream>
// 递归终止的空元组
template <typename... Args>
struct SimpleTuple {};
// 递归定义的元组,第一个元素类型为T,剩余元素类型为Rest...
template <typename T, typename... Rest>
struct SimpleTuple<T, Rest...> {
T first;
SimpleTuple<Rest...> rest;
SimpleTuple(T f, Rest... r) : first(f), rest(r...) {}
};
// 获取元组第一个元素的函数
template <typename T, typename... Rest>
T get_first(const SimpleTuple<T, Rest...>& t) {
return t.first;
}
int main() {
SimpleTuple<int, double, const char*> t(10, 3.14, "test");
std::cout << "元组第一个元素:" << get_first(t) << std::endl;
return 0;
}
使用注意事项
- 模板参数包的省略号位置需要注意,声明时
typename... Args中的省略号在参数名前,展开时Args... args的省略号在参数名后。 - 递归展开可变参数时,必须定义参数包为空的终止函数,否则会出现编译错误。
- 折叠表达式需要C++17及以上标准支持,编译时需要指定
-std=c++17或更高版本。 - 可变参数展开时如果需要处理每个参数的类型,可以结合
decltype获取参数类型做进一步操作。
常见问题解答
参数包可以为空吗
可以,模板参数包允许接收0个参数,此时需要对应的处理逻辑,比如递归展开的终止函数就是处理空参数包的情况。
可变参数和普通函数重载有什么区别
普通函数重载需要为每个参数数量或类型组合编写单独的函数,而可变参数模板可以一次性处理任意数量和类型的参数,代码复用性更高。
如何获取参数包的参数个数
可以通过sizeof...(Args)运算符获取参数包的参数个数,该运算符在编译期计算,返回值是编译期常量。
#include <iostream>
template <typename... Args>
void print_arg_count(Args... args) {
std::cout << "参数个数:" << sizeof...(Args) << std::endl;
}
int main() {
print_arg_count(1, 2, 3);
print_arg_count("a", 1, 2.5, 'x');
return 0;
}