目录
引言
在C++编程中,处理不确定数量的参数是一个常见的需求。为了支持这种需求,C标准库提供了 <stdarg.h>
头文件,其中定义了一组宏和类型,用于处理不定参数函数。C++继承了C语言的可变参数机制,使用了stdarg.h
提供的宏来处理不确定数量的参数。其原理基于栈的推入和弹出过程,不需要明确参数数量。此外,C++提供了可变参数机制,让我们能够创建接收任意数量参数的函数。这一特性在许多实际应用中非常有用,比如日志记录、函数重载等。
<stdarg.h>
库的基本功能
<stdarg.h>
库包含以下主要部分:
va_start
宏:
用于初始化 va_list
变量,其基本语法如下:
void va_start(va_list ap, last);
ap
:va_list
变量。last
:最后一个确定的参数,后面的参数是可变参数。
va_arg
宏
va_arg
宏用于访问可变参数列表中的下一个参数,其基本语法如下:
type va_arg(va_list ap, type);
ap
:va_list
变量。type
:要访问的参数的类型。
va_end 宏
va_end 宏用于结束 va_list 变量的访问,其基本语法如下:
void va_end(va_list ap);
- ap:va_list 变量。
va_copy 宏
va_copy 宏用于复制 va_list 变量,其基本语法如下:
void va_copy(va_list dest, va_list src);
- dest:目标 va_list 变量。
- src:源 va_list 变量。
使用 <stdarg.h>
处理可变参数代码
示例中,print_args
接收一个格式字符串,然后根据格式字符(i
表示整数,d
表示双精度浮点数)解析后面的参数。
#include <iostream>
#include <cstdarg>
void print_args(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
while (*fmt != '\0') {
if (*fmt == 'i') {
int i = va_arg(args, int);
std::cout << "int: " << i << std::endl;
} elseif (*fmt == 'd') {
double d = va_arg(args, double);
std::cout << "double: " << d << std::endl;
}
++fmt;
}
va_end(args);
}
int main()
{
print_args("ddii", 0.618,3.14, 7, 9);
return 0;
}
//double: 0.618 double: 3.14 int: 7 int: 9
C++11可变参数模板
基本概念
C++11通过模板提供了类型安全且灵活的可变参数机制。可以通过递归模板来处理不同类型的参数,避免了手动处理类型的麻烦。也就是支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包。
参数包有两种类型:
- 模板参数包,表示零或多个模板参数,使用class...或typename...关键字声明。
- 函数参数包,表示零个或多个函数参数,使用类型名后跟...表示。
template<class ...Arg> void Func(Arg... arg) {}
template<class ...Arg> void Func(Arg... arg) {}
template<class ...Arg> void Func(Arg... arg) {}
我们用省略号...来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表。可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
sizeof... 运算符
sizeof...运算符来计算参数包中参数的个数。
template <class ...Arg>
void PrintArgNum(Arg&&... arg)
{
cout << sizeof...(arg)<<"个参数包" << endl;
}
int main()
{
double a = 3.14;
PrintArgNum();
PrintArgNum(a);//一个参数
PrintArgNum(1, string("241564132"));//两个参数
return 0;
}
编译本质会结合引用折叠规则实例化出以下三个函数,在类型泛化基础上叠加了数量变化,让泛型编程更加灵活。
void Print();
void Print(double&& arg1);
void Print(int&& arg1, string&& arg2);
包扩展
对于一个参数包,我们除了计算它的参数个数,还可以对它进行包扩展。我们还要提供用于每个扩展元素的模式,扩展一个包就是将他分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放省略号(...)来触发扩展操作。
//参数包是0个时,直接匹配这个函数
void ShowList()
{
cout<< endl;
}
template<class T,class ...Arg>
void ShowList(T&& val,Arg&&... arg)
{
cout << val << endl;
//arg是N个参数的参数包,调用Printf,参数包的第一个传给val,剩下N-1个传给参数包,
ShowList(arg...);
}
template <class ...Arg>
void Print(Arg&&... arg)
{
cout << sizeof...(arg)<<"个参数包" << endl;
ShowList(arg...);
}
int main()
{
double x = 3.14;
Print();
Print(11.1);//一个参数
Print(1, string("bjkbhv"));//两个参数
Print(12.55, string("9jjug7"), x);//三个参数
return 0;
}
实际上是通过递归展开来实现的,当参数包为空时就会调用 void ShowList(),同时终止递归
递归时,T接受传来参数包的第一个参数类型,arg接受其余的参数类型,以此往复。
C++17折叠表达式
折叠表达式基本使用举例
//基础情况 - 只有一个参数时的处理
template<typename T>
T sum(T v) {
return v; // 递归的终止条件
}
//基础情况 - 只有一个参数时的处理
template<typename T>
T sum(T v) {
return v; // 递归的终止条件
}
//C++17 折叠表达式写法
template<typename... Args>
auto sum(Args... args) {
return (... + args);
}
编译时实际展开
sum(1, 2, 3, 4)
// C++11 可变参数递归版本展开:
1 + sum(2, 3, 4)
1 + (2 + sum(3, 4))
1 + (2 + (3 + sum(4)))
1 + (2 + (3 + 4))
// C++17 折叠表达式直接展开:
((1 + 2) + 3) + 4
可变参模板的缺点:
代码简洁度 : C++11可变参模板需要两个模板函数,而且还要写递归。新版本只需要一个函数,一行代码就搞定。
编译效率 :可变参数模板在使用时候需要写递归,递归版本每处理一个参数都要生成一次函数调用,而折叠表达式在编译期就能展开成一个扁平的表达式。
运行时性能 : 可变参数模板每个递归调用都会产生函数调用开销,而折叠表达式会被编译器优化成一组简单的加法运算。
折叠表达式四种基本语法
一元右折叠
//一元右折叠 - 从右往左折
(pack op ...)
// 例如: (args + ...) 会展开成 a1 + (a2 + (a3 + a4))
template<typename... Args>
void print_right(Args... args) {
// 从右向左展开: a1 + (a2 + (a3 + a4))
(std::cout << ... << args) // 从右向左展开
}
一元左折叠
//一元左折叠 - 从左往右折
(... op pack)
// 例如: (... + args) 会展开成 ((a1 + a2) + a3) + a4
template<typename... Args>
void print_left(Args... args) {
// 从左向右展开: ((a1 + a2) + a3) + a4
(args << std::cout) // 从左向右展开
}
二元右折叠
pack op ... op init)
//例如: (args + ... + 100) 变成 a1 + (a2 + (a3 + 100))
template<typename... Args>
auto sum_right(Args... args) {
return (args + ... + 100); // 右边带初始值: a1 + (a2 + (a3 + 100))
}
二元左折叠
(init op ... op pack)
// 例如: (100 + ... + args) 变成 ((100 + a1) + a2) + a3
template<typename... Args>
auto sum_left(Args... args) {
return (100 + ... + args); // 左边带初始值: ((100 + a1) + a2) + a3
}
op 可以用很多运算符
a. 算术运算符 - 做数学计算用
+, -, *, /, %b. 位运算符 - 处理二进制位
^, &, |, <<, >>c. 赋值运算符 - 存储值用
=, +=, -=, *=, /=, %=, ^=, &=, |=, <<=, >>=d. 比较运算符 - 判断大小关系
==, !=, <, >, <=, >=e. 逻辑运算符 - 处理真假值
&&, ||f. 其他特殊运算符
,(逗号), .*, ->*
空包参数注意事项
只有 &&、|| 和逗号运算符才能安全处理空参数包。
template<typename... Args>
bool all(Args... args) {
return (... && args); // 安全
// return (... + args); // 危险
}