最近在用C++开发项目过程中,遇到了场景需要用可变参(
...
)的情况,发现开发业务相关同事,对这块理解不是很清晰,遂对此进行梳理总结,以便业务相关同事学习,以及后续遇到时的参考。对此不是很清楚的读者也可参考看下。
文章目录
- 1. 变参函数(C风格)
- 例子1 可变参为数值类变量
- 例子2 可变参为字符串
- 2. 函数模板可变参(variadic templates)
- 3. 模板类使用可变参数
- 4. 折叠表达式(fold expressions) (C++17 引入)
1. 变参函数(C风格)
在C++中,开发者可以使用C风格的可变参函数来构建一个能接受任意数量参数的函数。这种方式允许函数处理不确定数量的参数,正如C语言中的printf
函数一样。C风格的可变参函数依赖于<cstdarg>
头文件,这个头文件提供了一组宏来操作可变参数列表。主要的宏包括va_start
、va_arg
、va_end
和va_copy
。
下面我们通过两个例子来看可变参函数的使用情况。
例子1 可变参为数值类变量
下面是一个基本的例子,展示如何使用C风格的可变参函数实现一个简单的函数,这个函数接受任意数量的整数参数,并计算它们的和:
#include <cstdarg>
#include <iostream>// 定义一个计算任意个整数和的函数
int sum(int n, ...) {int total = 0;va_list args;va_start(args, n); // 初始化args为参数列表,n是参数个数for(int i = 0; i < n; ++i) {total += va_arg(args, int); // 使用va_arg获取下一个参数的值}va_end(args); // 清理工作return total;
}int main() {int result = sum(5, 1, 2, 3, 4, 5); // 可以传入任意数量的参数std::cout << "The sum is: " << result << std::endl;return 0;
}
输出将会是:
The sum is: 15
在上述代码中,sum
函数的第一个参数n
是后续参数的个数,之后的参数被视为一个可变参数列表。可变参数列表使用va_list
类型的变量args
来访问。我们首先使用va_start
宏初始化args
,以使其指向第一个可变参数。接着,通过在循环中反复调用va_arg
宏来逐一访问每个参数;此宏的第二个参数表明了当前要访问的参数的类型。最后,使用va_end
宏对args
进行清理工作。
例子2 可变参为字符串
如果开发者想要使用C风格的可变参函数来输出字符串,类似于C语言中的printf
函数,开发者可以使用vprintf
函数。vprintf
和printf
类似,但它接收一个va_list
类型的参数来处理可变数量的参数。
下面是一个例子,演示了如何实现一个可变参函数来记录日志信息:
#include <cstdarg>
#include <cstdio>// 定义一个日志函数
void log_message(const char* format, ...) {va_list args;// 开始处理可变参数va_start(args, format);// vprintf使用提供的格式化字符串和可变参数列表进行输出vprintf(format, args);// 输出换行printf("\n");// 清理可变参数列表va_end(args);
}#define LOG_MSG(...) log_message(__VA_ARGS__)int main() {LOG_MSG("This is a %s message", "test");LOG_MSG("Today is %s, %d degrees celsius", "Tuesday", 25);// log_message("This is a %s message", "test");// log_message("Today is %s, %d degrees celsius", "Tuesday", 25);return 0;
}
输出将会是:
This is a test message
Today is Tuesday, 25 degrees celsius
在上述代码中,log_message
函数接受了一个format
字符串,这个字符串包含了格式化信息。这些格式编码指示了如何打印后续的参数(例如,%s
表示字符串,%d
表示整数)。函数用va_start
宏初始化va_list
类型的变量args
,然后使用vprintf
将格式化的字符串与可变参数一起输出。最后用va_end
宏清理可变参数。
2. 函数模板可变参(variadic templates)
C++中的函数模板可变参允许开发者创建接受任意数量参数的函数模板,甚至可以是不同类型的参数。这是通过模板参数包(template parameter pack
)和函数参数包(function parameter pack
)来实现的。在C++11及以后的版本中,使用省略号...
来表示参数包。
下面是一个使用函数模板可变参的例子,使用递归函数模板打印出可变数量的参数:
#include <iostream>// 基本函数模板,用于终止递归
void print() {// 当没有参数时,就什么都不做,终止递归std::cout << "No more arguments." << std::endl;
}// 可变参数模板,用于打印一个参数然后递归调用自身以打印剩余参数
template<typename T, typename... Args>
void print(T firstArg, Args... args) {std::cout << firstArg << std::endl; // 打印第一个参数print(args...); // 递归调用自身以打印剩余参数
}int main() {print(1, 2.5, "three");return 0;
}
输出将是:
1
2.5
three
No more arguments.
在上面的代码中,print
函数模板使用两个参数:第一个是类型为T
的firstArg
,表示当前函数需要处理的第一个参数;第二个是一个函数参数包args...
,代表其余的参数。在函数模板内部,第一个参数被打印出来,然后通过递归调用print
函数模板来处理剩余的参数。为了终止递归,我们提供了一个重载的print
函数,它不接受任何参数。当参数包中没有剩余元素时,会调用这个重载的函数。
3. 模板类使用可变参数
C++中的模板类使用可变参,允许编写能够接受任意数量类型参数的类模板。它通过使用模板参数包(template parameter pack)实现,这在实现泛型编程时非常有用。
下面是一个使用了可变参的模板类示例,我们将创建一个简单的元组类MyTuple
,用于存储任意数量和类型的元素:
#include <iostream>
#include <string>// 前向声明,用于递归展开模板参数包
template<typename... Ts>
class MyTuple;// 专门化终止条件
template<>
class MyTuple<> {};// 主模板
template<typename T, typename... Ts>
class MyTuple<T, Ts...> {T head;MyTuple<Ts...> tail;public:MyTuple(T head, Ts... tail) : head(head), tail(tail...) {}// 获取第一个元素T getHead() const { return head; }// 获取尾部元组MyTuple<Ts...> getTail() const { return tail; }// 打印第一个元素和递归打印剩余元素void print() const {std::cout << head << " ";tail.print();}
};// 递归终止条件的打印函数
template<>
void MyTuple<>::print() const {}int main() {MyTuple<int, double, std::string> myTuple(1, 2.3, "Hello World");myTuple.print();return 0;
}
在这个示例中,MyTuple
是一个模板类,可以接受任意数量和任意类型的参数。这是通过在类定义时使用模板参数包typename... Ts
实现的。模板参数包是C++11引入的特性,它允许函数或类模板接受不确定数量的模板参数。
MyTuple
类内部,我们使用了递归组合的方式来处理这些参数,其中每个MyTuple
对象存储一个元素(head
)和一个剩余元素的元组(tail
)。通过这种方式,我们可以实现getHead()
和getTail()
成员函数,分别用于获取头部元素和尾部元组。
最后,我们通过特化一个空的MyTuple<>
类和提供一个递归终止条件下的print
方法,来处理递归展开的终点情况。
运行上述程序的输出将会是:
1 2.3 Hello World
4. 折叠表达式(fold expressions) (C++17 引入)
在C++中,折叠表达式是C++17标准中引入的一项功能,它允许程序员对包展开时产生的所有参数进行一个二元操作。这大大简化了可变参数模板函数或类模板的编写,因为开发者不再需要写递归模板或递归函数来处理参数包。
一个简单的例子可以解释如何使用折叠表达式:
#include <iostream>// 可变参数模板函数,使用折叠表达式来计算所有参数之和
template<typename... Args>
auto sum(Args... args) {return (... + args);
}int main() {std::cout << "Sum of 1, 2, 3 is " << sum(1, 2, 3) << std::endl;std::cout << "Sum of 10, 20, 30, 40 is " << sum(10, 20, 30, 40) << std::endl;return 0;
}
这个sum
函数通过折叠表达式计算了任意数量整数参数的和。这里的(... + args)
是一个展开表达式,它将传递给sum
的所有参数进行累加操作。
在这个表达式中:
...
表示展开操作符+
是要应用的二元操作args
是参数包
折叠表达式有两种形式:
- 二元左折叠(Binary Left Fold):
(op ... args)
,它展开为((arg1 op arg2) op ...) op argN
。 - 二元右折叠(Binary Right Fold):
(args ... op)
,它展开为arg1 op (arg2 op ... (argN-1 op argN))
。
运行上述代码将会得到以下输出:
Sum of 1, 2, 3 is 6
Sum of 10, 20, 30, 40 is 100
这展示了折叠表达式是处理可变参数模板函数一个非常强大且简洁的方法。它不仅限于求和,还可以用于其它任何二元操作,如乘法、逻辑操作等。