一,可变参数
1.基础概念
可变参数在C语言和C++语言编程中都有应用。
可变参数的含义是:在函数传参的时候,参数的数量、类型都是可变的,不确定的。
在C语言中,应用到可变参数的是可变参数函数和可变参数的宏。
在C++语言中,C++11标准提供了两种使用可变参数的方式:
1.如果可变参数的参数类型相同,可以使用标准库中的initializer_list。
2.如果可变参数的参数类型不同,可以使用可变参数模板。
C语言中,在定义可变参数函数时,使用省略号"..."表示参数是可变的。
简单代码样例如下:
void printf(const char* format, …);
可变参数的使用可以让代码结构更精简。
2.可变参数相关的宏定义
在C语言中,一般需要借助相关的宏定义来实现可变参数,常见的宏定义如下:
va_arg:每一次调用va_arg会获取当前的参数,并自动更新指向下一个可变参数。
va_start:获得可变参数列表的第一个参数,开始使用可变参数列表。
va_end:结束对可变函数列表的遍历,释放va_list。
va_list:存储可变参数列表的具体信息。
简单介绍就是,va_start用于开始使用可变参数,va_arg用于获得下一个可变参数,va_end用于释放va_list。
它们都包含在头文件"<stdarg.h>"中
这些宏定义在具体应用时的语法如下:
type va_arg( | |
va_list arg_ptr, | |
type | |
); | |
void va_end( | |
va_list arg_ptr | |
); | |
void va_start( | |
va_list arg_ptr, | |
prev_param | |
); | |
void va_start( | |
arg_ptr | |
); // (deprecated Pre-ANSI C89 standardization version) |
注意,如果自定义参数和可变参数同时在函数中出现,为了不导致编译出错,将可变参数放在形参列表的最后一个位置。
void func(char parm_1, int parm_2, ...);
完整代码样例:
void vout(int max, ...) | |
{ | |
va_list arg_ptr; | |
int args = 0; | |
char* days[7]; | |
va_start(arg_ptr, max); | |
while (args < max) | |
{ | |
days[args] = va_arg(arg_ptr, char*); | |
printf("Day: %s \n", days[args++]); | |
} | |
va_end(arg_ptr); | |
} | |
int main(void) | |
{ | |
vout(3, "Sat", "Sun", "Mon"); | |
printf("\n"); | |
vout(5, "Mon", "Tues", "Wed", "Thurs", "Fri"); | |
} |
运行结果:
Day: Sat | |
Day: Sun | |
Day: Mon | |
Day: Mon | |
Day: Tues | |
Day: Wed | |
Day: Thurs | |
Day: Fri |
3.预定义标识符_VA_ARGS__
对于可变参数相关的代码编写,除了使用省略号来表示可变参数列表,也可以使用__VA_ARGS__ 预定义标识符来表示可变参列表。
该语法在C99标准中被引入,可以简单了解一下。
可以用"__VA_ARGS__"表示"..."位置的所有参数,用法如下:
#define PRINT(...) printf(__VA_ARGS__)
完整代码样例:
int main(void) | |
{ | |
const char* s = "abc"; | |
int n = 123; | |
PRINT("%d\n", n); | |
PRINT("%s %d\n", s, n); | |
} |
运行结果:
123 | |
abc 123 |
二,标准库模板initializer_list
initializer_list模板在函数声明中可以代表可变参数列表。
initializer_list中的参数可以使用迭代器来访问。
initializer_list实例中传入参数时需要使用{}把多个参数括起来。
代码样例:
initializer_list<int> i1{ 1, 2, 3, 4 };
Demo1: 初始化类成员
class Point { | |
std::vector<int> arr; | |
public: | |
//Constructor accepts a initializer_list as argument | |
Point(const std::initializer_list<int>& list) : arr(list) | |
{} | |
void display() { | |
for (int i : arr) | |
std::cout << i << " , "; | |
std::cout << std::endl; | |
} | |
}; | |
int main() { | |
Point pointobj({ 1, 2, 3, 4, 5 }); | |
pointobj.display(); | |
return 0; | |
} |
运行结果:
1 , 2 , 3 , 4 , 5 ,
Demo2:结合lambda表达式一起使用
using namespace std; | |
template<typename... Args> | |
void print(Args... args) | |
{ | |
std::initializer_list<int>{ | |
([&] { cout << args << " "; }(), 0)... | |
}; | |
} | |
int main() | |
{ | |
print(1, 2, "3A", 4); | |
return 0; | |
} |
运行结果:
1 2 3A 4
三,可变参数模板
1.基础概念
可变参数模板是支持任意数量和类型的参数的类模板或函数模板。
在可变参数模板中,可变数目和类型的参数列表被称为参数包(parameter pack)。
可变参数模板的参数包,分为模板参数包(template parameter pack)和函数参数包(function parameter pack)。
在模板参数位置的可变参数被称为模板参数包,在函数参数位置的可变参数被称为函数参数包。
可以使用sizeof...运算符获取参数包中具体的参数数量。
样例如下:
//Args是一个模板参数包;args是一个函数参数包 | |
template <typename... Args> | |
void func(Args... args); |
如上所示,在一个模板参数列表中:
class...或typename...表示接下来的参数是零个或多个类型列表。
类型名...表示接下来的参数是零个或多个给定类型的函数参数列表。
比较一下"typename T"和"typename.. Args":
Args和T的差别是,T与一种类型匹配,而Args与任意数量(包括零)的类型匹配。
完整代码样例:
Demo1:
template <typename T> | |
void printAllImpl(T item) { | |
std::cout << item << ' '; | |
} | |
template <typename T, typename ...Args> | |
void printAllImpl(T item, Args ... args) { | |
printAllImpl(item); | |
printAllImpl(args...); | |
} | |
template <typename... Args> | |
void printAll(Args&&... args) { | |
printAllImpl(std::forward<Args>(args) ...); | |
std::cout << '\n'; | |
} | |
int main() { | |
printAll(3, 2, 1); | |
printAll(8.2, 2, 1.1, "A"); | |
printAll(23, 32, 8, 11, 9); | |
} |
运行结果:
3 2 1 | |
8.2 2 1.1 A | |
23 32 8 11 9 |
2.参数包的递归解析
可变参数列表中,参数包的展开方式为递归展开,即将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给相同函数递归调用,以此类推,直到参数列表为空。
代码样例:
template<typename T, typename... Args> | |
void show_list(T value, Args... args) | |
{ | |
std::cout << value << ", "; | |
show_list(args...); //递归调用 | |
} | |
int main() | |
{ | |
int n = 2; | |
double m = 3.0; | |
std::string str = "test"; | |
show_list(1, n, m); | |
show_list(1, n, m * m, str); | |
return 0; | |
} |
以上代码在VS2019中运行时,会报以下编译错误:
“show_list”: 未找到匹配的重载函数 | |
“void show_list(T,Args...)”: 应输入2个参数,却提供了0个 |
出现以上问题的原因是,可变参数函数模板通常是递归的。函数在第一次调用时,会使用参数包中的第一个实参,然后递归调用自身来陆续使用参数包中的剩余实参。为了终止递归,我们还需要定义一个非可变参数的函数模板或者普通函数。
以下代码都包含终止递归的函数模板。
Demo1:
//用来终止递归并处理参数包中最后一个元素 | |
template<typename T> | |
void show_list(T value) | |
{ | |
std::cout << value << ", "; | |
} | |
//参数包中除了最后一个元素之外的其他元素都会调用这个版本的show_list | |
template<typename T, typename... Args> | |
void show_list(T value, Args... args) | |
{ | |
std::cout << value << ", "; | |
show_list(args...); //递归调用 | |
} | |
int main() | |
{ | |
int n = 2; | |
double m = 3.0; | |
std::string str = "test"; | |
show_list(1, n, m); | |
show_list(1, n, m * m, str); | |
return 0; | |
} |
运行结果:
1, 2, 3, 1, 2, 9, test,
Demo2:
void tprintf(const char* format) //终止递归调用 | |
{ | |
std::cout << format; | |
} | |
template<typename T, typename... Targs> | |
void tprintf(const char* format, T value, Targs... Fargs) | |
{ | |
for (; *format != '\0'; format++) | |
{ | |
if (*format == '%') | |
{ | |
std::cout << value; | |
tprintf(format + 1, Fargs...); //递归调用 | |
return; | |
} | |
std::cout << *format; | |
} | |
} | |
int main() | |
{ | |
tprintf("% world% %\n", "Hello", '!', 123); | |
} |
运行结果:
Hello world! 123
特殊情况,当不涉及"typename T"的使用时,可以不需要单独定义一个非可变参数的函数模板来终止递归。
Demo3:
using namespace std; | |
template<typename... Argv> | |
void print_func(Argv... argv) | |
{ | |
cout << "print_func() is called with " | |
<< sizeof...(Argv) | |
<< " argument(s)." << endl; | |
} | |
int main(void) | |
{ | |
print_func(); | |
print_func(4, "a"); | |
print_func("a", "b", "c"); | |
return 0; | |
} |
运行结果:
print_func() is called with 0 argument(s). | |
print_func() is called with 2 argument(s). | |
print_func() is called with 3 argument(s). |
3.参数包展开过程拆解
演示代码:
using namespace std; | |
void print() | |
{ | |
cout << "I am empty.\n"; | |
} | |
template <typename T, typename... Types> | |
void print(T var1, Types... var2) | |
{ | |
cout << var1 << endl; | |
print(var2...); | |
} | |
int main() | |
{ | |
print(1, 2, 3.14, "test"); | |
return 0; | |
} |
过程拆解:
main函数中,第一次调用print,传递的实参:1,参数包剩余元素:2, 3.14, "test"。 | |
第一次递归调用print,传递的实参:2,参数包剩余元素:3.14, "test" | |
第二次递归调用print,传递的实参:3.14,参数包剩余元素:"test" | |
第三次递归调用print,传递的实参:"test",参数包中的元素已全部用完。 | |
由于参数包中的元素为空,退出递归,最后调用的是具体函数print()。 |
运行结果:
1 | |
2 | |
3.14 | |
test | |
I am empty. |
4.sizeof...运算符
由于带有"typename T"参数的可变参数的模板函数,总是需要再定义一个同名的模板函数或者普通函数来搭配使用,使得代码特别重复。
为了解决以上问题,可以使用"sizeof..."运算符来保证,在不重复定义同名函数的情况下让递归退出。
"sizeof..."运算符可以判断参数包中的元素数量。
退出递归的方式: 判断当参数包的元素个数为零时,退出函数调用。
sizeof...用法演示:
template<class...A> | |
int func(A...arg) { | |
return sizeof...(arg); | |
} | |
int main(void) { | |
if (func<int>(1, 2, 3, 4, 5) == 5) { | |
printf("the num of arg is 5"); | |
} | |
return 0; | |
} |
运行结果:
the num of arg is 5
sizeof...在结束递归中的使用
Demo1:
template<typename T, typename... Args> | |
void print_2(T value1, Args... args) { | |
std::cout << value1 << ", "; | |
if(sizeof...(args) > 0) { | |
print_2(args...); | |
} | |
} | |
int main() | |
{ | |
print_2(1, 2, "A"); | |
} |
以上用法无法导致递归终止,而且还会引起编译报错,原因是if判断无法在该函数模板中生效。
为了解决以上问题,C++17标准中引入了编译期if条件判断的表达式"if constexpr"。
Demo2:
template<typename T, typename... Args> | |
void print_2(T value1, Args... args) { | |
std::cout << value1 << ", "; | |
if constexpr (sizeof...(args) > 0) { | |
print_2(args...); | |
} | |
} | |
int main() | |
{ | |
print_2(1, 2, "A"); | |
} |
运行结果:
1, 2, A,
四,参考阅读
《C++17入门经典》
《C++ primer》
《深入理解C++11》
https://www.sandordargo.com/blog/2023/05/03/variadic-functions-vs-variadic-templates
https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/va-arg-va-copy-va-end-va-start?view=msvc-170
https://www.ibm.com/docs/en/zos/2.3.0?topic=lf-va-arg-va-copy-va-end-va-start-access-function-arguments
https://www.sandordargo.com/blog/2023/05/03/variadic-functions-vs-variadic-templates
https://en.cppreference.com/w/cpp/language/parameter_pack