C 程序员肯定接触过可变参数,毕竟我们都用过printf,但是直到C 11时C 才推出真正意义上的可变参数。
可变参数通过可变参数模板实现,在C 11中通过递归调用,借助编译器生成多个递归的特化函数,调用时依次展开。C 17中引入折叠表达式,简化了可变参数的实现方式,但仍经由编译器生成了对应的特化函数。
基本概念
- 形参包(Parameter Pack): 形参包是接受零个或多个模板实参(非类型、类型或模板)的模板形参,分为类型形参包(如typename... Args)和非类型形参包(如int... values)。
- 递归展开: 通过递归调用函数或模板,每次调用时从形参包中移除一个或多个参数,直至形参包为空,完成所有参数的处理。
//类型形参包
template<typename... Args> // Args 是一个类型形参包
class Tuple {
// ...
};
template<typename... Args> // Args 是一个类型形参包
auto sum(Args... args) {
// ...
};
//非类型形参包示例
template<int... Values> // Values 是一个非类型形参包
struct Sum {
//....
};
由上文知道,可变参数存在两种实现方式,递归展开和折叠表达式。接下来将分别说明如下:
递归展开
可变参数在C 17前仅支持递归展开,通过逐步处理形参包直到其为空。示例见如下的print函数,
代码语言:javascript复制// 特殊化处理0参数情况
void print() {
// 可选:处理无参数时的逻辑,比如打印结束符
std::cout << "n" ;
}
//终止条件
template<typename T>
auto print(T a)
{
std::cout << a << "n";
}
//变参模板
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << ", ";
print(args...); // 递归调用,形参包展开
}
// 使用示例
print(1, 2, 3, 4, 5);
在上面的代码中,
- print函数首先定义了一个终止条件,当只有一个参数时直接打印该参数并结束。
- 定义了接受一个或多个参数的模板,其中first是第一个参数,args...是剩余参数的形参包。通过递归调用自身并传入剩余参数,直到形参包为空。
- 但如上的两个函数存在一个缺陷——无法处理0个参数的场景,所以增加具有0个参数的函数,其也可视为模板参数的0个参数的特化版本。
折叠表达式
C 17引入了更简洁的形参包展开语法,折叠表达式(Fold Expressions):
代码语言:javascript复制template<typename... Args>
auto sum(Args... args)
{
if constexpr (sizeof...(Args) )//形参包非空
{
return (args ...); // 折叠表达式,等价于 args[0] args[1] ... args[N]
}
else
{
// 没有参数,处理空情况
std::cout << "No arguments provided." << std::endl;
}
}
如上代码借助sizeof...方法获得形参包中形参数量,区别处理形参包为空——0个参数的场景。
注意事项
可变参数由于其可输入任意长度参数,方便了用户,但其也存在自身的劣势,所以在使用时需要注意:
- 性能考量:采用递归展开模式时,编译器生成多个递归调用的模板特化函数,过度使用可变参数可能增加编译时间和代码体积。
- 类型安全:C 强类型系统意味着可变参数模板在使用时必须确保类型安全。
- 边界条件:设计可变参数函数时,通常需要提供一个终止递归的边界条件。
结论
可变参数模板是C 现代编程不可或缺的一部分,本文结合代码分别介绍了递归调用和折叠表达式两种实现方式。由于多参数时折叠表达式生成的模板特化函数的数量远少于递归生成的特化函数数量(5个参数的递归展开将产生5个模板特化,而折叠表达式只有1个特化)同时编译器也基本都支持C 17了,建议使用折叠表达式的实现方式.