可变参数是指在函数或模板中可以接受任意数量的参数。在C++中,可变参数通常使用模板和递归的方式来实现,允许函数或模板处理不定数量的参数。
例如,在C++标准库中自己实现的max函数,min函数等都是实现一个两个对象比较,但是如果我们想要自己实现多个参数呢?在模板元编程中可变参数也有很多作用。
可变参数的作用是允许函数或模板处理不定数量的参数,从而实现更灵活的函数调用和模板实例化。这样可以简化代码,提高代码的复用性,并且能够处理各种不同数量的参数。
我们首先看第一个例子
递归使用可变参数
void print() {
}
template<typename T, typename... Types>
void print(T firstArg, Types... args) {
cout << firstArg << '\n';
print(args...);
}
int main() {
print(1, '1', "b");
}
当程序执行到 print(1, '1', "b"); 这一行时,会调用 print 函数模板的实例化版本,其中 T 被推导为 int,firstArg 被赋值为 1,args 被推导为 char 和 const char* 类型的参数包。
然后,实例化的 print 函数会打印 firstArg 的值,即 1,然后递归调用 print 函数,将剩余的参数包 args 传递给下一个实例化的 print 函数。
递归调用的 print 函数会以同样的方式处理参数包,直到参数包为空,递归结束。
这样,整个过程就实现了对任意数量参数的打印。
那么第一行的print()有什么作用呢?
第一个print()函数是一个基础情况,用于处理可变参数模板中的递归终止条件。当参数包args为空时,递归调用会停止,这时就会调用这个空的print()函数,起到终止递归的作用。
如果我们删除这个print那么编译器就会报错,因为这里采用的是递归的方式,必须有一个基本状态。
那么可以不使用这种方式吗?
折叠表达式使用可变参数
template<typename T, typename... Types>
void print(T firstArg, Types... args) {
((cout << firstArg << '\n') , ... , (cout << args << '\n'));
}
在这里我们使用一个折叠表达式来达到一样的效果,
折叠表达式的语法如下:
(expression op ... op expression)
其中,op是折叠操作符,也可以是+、-、*、/等二元运算符,也可以是&&、||等逻辑运算符。注意 ‘ , ’ 是一个特殊的运算符,如果我们使用 (firstArg , ... , args)进行赋值操作,那么被赋值只会等于args,因此,使用 ‘,’ 进行折叠表达式只能进行操作,而不能进行赋值等类似的操作。
使用std::initializer_list处理(单一类型)
#include <iostream>
#include <initializer_list>
void print(std::initializer_list<int> args) {
for (int arg : args) {
std::cout << arg << '\n';
}
}
int main() {
print({1, 2, 3, 4, 5});
return 0;
}
使用tuple(可以多类型)
template <typename... Args>
void print(const std::tuple<Args...>& args) {
std::apply([](const auto&... arg){((std::cout << arg << '\n'), ...);}, args);
}
int main() {
print(std::make_tuple(1, '1', "b"));
return 0;
}
注意事项:
可变参数的一个重要特点是需要使用递归、折叠表达式或其他方式来处理参数包。此外,需要注意的特点包括:
1. 参数包的展开:需要使用递归、折叠表达式或其他方式来展开参数包,以便对每个参数进行处理。
2. 参数包的顺序:在处理参数包时,需要注意参数的顺序,确保按照期望的顺序进行处理。
3. 参数包的类型:需要考虑参数包中参数的类型,以便在模板函数或其他函数中正确处理不同类型的参数。
这些特点需要在处理可变参数时特别注意,以确保正确处理任意数量的参数。
如何进行约束概念?
很多时候我们使用可变参数,有时候我们希望约束一个概念和范围。例如我们定义了一个func函数用来求累加,如下代码:
template<typename T, typename ...Types>
auto func(T firstargs,Types... args) {
return (firstargs + ... + args);
}
int main() {
cout<<func(1, 3, "1");
}
requires(is_integral_v<T> && ... && is_integral_v<Types>)
就能预防错误,这里我们的IDE(vs2022)就直接提供了错误信息,尝试运行编译器也会给我们错误信息
同时注意requires必须也得以折叠表达式(或者可以尝试写一个递归的require条件)展开,如果只是对于T进行约束,或者同时约束T和Types,前者只会对第一个参数进行约束,后者会忽略最后一个参数的约束条件(导致错误)。