在 C++ 中实现可变参数有多种方法,我为你详细介绍几种主要的方式:
1. C 风格可变参数(不推荐)
cpp
#include <cstdarg>
#include <iostream>
void printNumbers(int count, ...) {
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++) {
int num = va_arg(args, int);
std::cout << num << " ";
}
va_end(args);
std::cout << std::endl;
}
// 使用
printNumbers(3, 1, 2, 3); // 需要手动指定参数个数
缺点:类型不安全,需要手动管理参数,容易出错。
2. 可变参数模板(C++11 推荐)
这是最现代、最安全的方式。
基本用法
cpp
#include <iostream>
// 递归终止函数
void print() {
std::cout << std::endl;
}
// 可变参数模板函数
template<typename T, typename... Args>
void print(T first, Args... args) {
std::cout << first << " ";
print(args...); // 递归调用
}
// 使用
print(1, 2.5, "hello", 'A'); // 输出:1 2.5 hello A
使用折叠表达式(C++17)
cpp
template<typename... Args>
void print(Args&&... args) {
((std::cout << args << " "), ...); // 折叠表达式
std::cout << std::endl;
}
3. std::initializer_list(固定类型)
cpp
#include <iostream>
#include <initializer_list>
void print(std::initializer_list<int> values) {
for (auto val : values) {
std::cout << val << " ";
}
std::cout << std::endl;
}
// 使用
print({1, 2, 3, 4, 5});
4. 实际应用示例
实现格式化字符串
cpp
#include <iostream>
#include <sstream>
#include <string>
template<typename... Args>
std::string format(const std::string& fmt, Args... args) {
char buffer[256];
snprintf(buffer, sizeof(buffer), fmt.c_str(), args...);
return std::string(buffer);
}
// 使用
auto str = format("Name: %s, Age: %d, Score: %.2f", "Alice", 25, 95.5);
通用求和函数
cpp
#include <iostream>
// C++17 折叠表达式
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // 右折叠
}
// C++11/14 版本
template<typename T>
T sum(T t) {
return t;
}
template<typename T, typename... Args>
T sum(T first, Args... args) {
return first + sum(args...);
}
// 使用
auto total = sum(1, 2, 3, 4, 5); // 15
auto total2 = sum(1.1, 2.2, 3.3); // 6.6
创建任意参数的类
cpp
#include <iostream>
#include <vector>
#include <string>
class Logger {
public:
template<typename... Args>
void log(Args... args) {
std::ostringstream oss;
(oss << ... << args); // C++17 折叠表达式
logs.push_back(oss.str());
}
void showLogs() {
for (const auto& log : logs) {
std::cout << log << std::endl;
}
}
private:
std::vector<std::string> logs;
};
// 使用
Logger logger;
logger.log("Error", " at line ", 25, ", value: ", 3.14);
logger.log("User", " ", "Alice", " logged in");
5. 参数包展开技巧
cpp
#include <iostream>
// 1. 递归展开
template<typename T>
void process(T t) {
std::cout << t << std::endl;
}
template<typename T, typename... Args>
void process(T t, Args... args) {
std::cout << t << ", ";
process(args...);
}
// 2. 逗号运算符展开
template<typename... Args>
void printAll(Args... args) {
int dummy[] = { (std::cout << args << " ", 0)... };
std::cout << std::endl;
}
// 3. Lambda 表达式展开(C++14+)
template<typename... Args>
void processWithLambda(Args... args) {
([](auto arg) {
std::cout << arg << " ";
}(args), ...);
std::cout << std::endl;
}
6. 类型安全的变体
cpp
#include <iostream>
#include <variant>
#include <vector>
using VariantParam = std::variant<int, double, std::string>;
void processVariadic(const std::vector<VariantParam>& params) {
for (const auto& param : params) {
std::visit([](auto&& arg) {
std::cout << arg << " ";
}, param);
}
std::cout << std::endl;
}
// 使用
processVariadic({1, 3.14, "hello", 42});
选择建议
- C++11 及以上:优先使用可变参数模板,类型安全且灵活
- 同类型参数 :考虑使用
std::initializer_list - C++17:使用折叠表达式简化代码
- 避免:C 风格的可变参数,除非需要与 C 代码交互
可变参数模板是 C++ 中实现可变参数最强大和安全的方式,虽然语法看起来复杂,但提供了完全的编译时类型检查。
问得很好!这是理解可变参数模板的关键点。... 的位置很重要,让我详细解释:
... 的三个位置及含义
1. 模板参数声明中:typename... Args
cpp
template<typename... Args> // ✔ 正确:声明参数包
template<typename Args...> // ❌ 错误:...必须在类型名后面
这里的 ... 表示 声明一个模板参数包,可以接受任意数量的类型参数。
2. 函数参数声明中:Args... args
cpp
template<typename... Args>
void func(Args... args) { // ✔ 正确:声明函数参数包
// ...
}
template<typename... Args>
void func(args... Args) { // ❌ 错误:参数名在前,包名在后
// ...
}
3. 展开参数包时:args...
cpp
template<typename... Args>
void func(Args... args) {
// 展开参数包
otherFunc(args...); // ✔ 正确:展开所有参数
// 部分展开
otherFunc(args...); // 展开所有
otherFunc(args)...; // ❌ 错误:语法不对
}
完整的位置示例
cpp
// 声明处:typename... 在类型前面
template<typename... Types> // Types 是类型参数包
// 函数参数处:Types... 在变量名前面
void process(Types... values) { // values 是函数参数包
// 使用处:values... 在函数调用中展开
print(values...); // 展开所有参数
// 或者用递归方式
print(values...); // 正确
}
具体使用场景对比
场景1:直接传递所有参数
cpp
template<typename... Args>
void forward(Args... args) {
// args... 在函数调用内部
targetFunction(args...); // 展开所有参数
}
场景2:递归处理(C++11/14风格)
cpp
// 基本情况
void print() {
std::cout << std::endl;
}
// 递归情况:第一个参数在前,剩余参数包在后
template<typename T, typename... Args>
void print(T first, Args... rest) { // first 是单个参数,rest 是参数包
std::cout << first << " ";
print(rest...); // 展开剩余参数
}
场景3:折叠表达式(C++17)
cpp
template<typename... Args>
void print(Args... args) {
// ... 在表达式中间
((std::cout << args << " "), ...); // 右折叠
// 或者
(..., (std::cout << args << " ")); // 左折叠
std::cout << std::endl;
}
常见的 ... 位置模式
cpp
// 1. 模板声明:typename... 包名
template<typename... Args>
// 2. 函数声明:包名... 参数名
ReturnType functionName(Args... args)
// 3. 函数调用:参数名...
otherFunction(args...);
// 4. 递归调用:参数包...
function(rest...);
// 5. 折叠表达式:(表达式 ...) 或 (... 表达式)
(args + ...) // 右折叠:arg1 + (arg2 + (arg3 + arg4))
(... + args) // 左折叠:(((arg1 + arg2) + arg3) + arg4)
记忆口诀
- 声明时 :
typename...在类型前面 - 参数时 :
Args...在变量名前面 - 使用时 :
args...作为整体展开
错误示例分析
cpp
// ❌ 错误:... 位置不对
template<typename Args...> // 应该在typename后面,Args前面
void func(...Args args) // 应该在Args后面,args前面
void func(args... Args) // 完全反了
otherFunc(...args); // 不应该有单独的...
otherFunc(args...); // ✔ 正确:展开所有参数
实际应用示例
cpp
#include <iostream>
#include <tuple>
// 正确示例
template<typename... Args>
auto make_tuple(Args... args) { // Args... args ✔
return std::make_tuple(args...); // args... ✔
}
// 转发示例
template<typename... Args>
void wrapper(Args&&... args) { // Args&&... args ✔
// 完美转发
target(std::forward<Args>(args)...); // args... ✔
}
int main() {
// 使用
auto t = make_tuple(1, 3.14, "hello"); // 自动推导Args...
// 查看结果
std::cout << std::get<0>(t) << " "
<< std::get<1>(t) << " "
<< std::get<2>(t) << std::endl;
return 0;
}
简单记法 :在声明时,... 紧跟在 typename 或 Args 后面;在使用时,... 紧跟在参数包名字后面。