一、解决什么问题?
问题:在 C++03 中,你想写一个"接收任意数量参数的函数",只能靠:
- 函数重载(写 N 个版本,累死)。
- C 风格的
...(va_args,类型不安全)。
Variadic Templates 的目标:类型安全的任意参数。
二、什么时候用它?
| 场景 | 例子 |
|---|---|
| 包装/转发函数参数 | std::make_unique<T>(args...)、std::thread(func, args...) |
| 实现类型安全的 printf | std::format("{} {}", a, b) |
| 构造 tuple | std::tuple<int, double, string>(1, 2.0, "hello") |
| 实现工厂模式 | factory.create<Widget>(arg1, arg2, arg3) |
| 日志库 | log.info("user {} logged in at {}", name, time) |
三、递归展开:编译器是怎么处理"任意参数"的?
// 你想写一个打印任意数量、任意类型参数的函数:
print(1, 2.0, "hello", 'c');
// Variadic Templates 的做法:递归 + 终止
void print() {} // ① 终止函数:0 个参数时调用
template <typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " "; // 打印第一个
print(rest...); // ② 用剩余参数继续递归
}
编译器实例化的过程:
print(1, 2.0, "hello", 'c')
编译器生成:
void print<int, double, const char*, char>(int first, ...) {
cout << first; // 打印 1
print<double, const char*, char>(2.0, "hello", 'c');
}
再生成:
void print<double, const char*, char>(double first, ...) {
cout << first; // 打印 2.0
print<const char*, char>("hello", 'c');
}
... 直到:
void print<>() { } // 终止
关键理解:print(1, 2.0, "hello") 在编译期被展开为3 个不同的函数,每个函数处理一个参数。没有运行时循环,没有类型擦除,每个参数类型都保留。
四、你最常用的场景:完美转发 + Variadic
// make_unique 的简化实现
template <typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
return std::unique_ptr<T>(
new T(std::forward<Args>(args)...) // 包展开 + 完美转发
);
}
auto ptr = my_make_unique<Widget>(1, 2.0, "config");
// 展开为:new Widget(std::forward<int>(1), std::forward<double>(2.0), std::forward<const char*>("config"))
// 每个参数的左值/右值属性都被完美保留
这里 std::forward<Args>(args)... 包展开做了两件事:
- 把
args包展开为arg1, arg2, arg3, ...。 - 对每个参数应用
std::forward,保持其左值/右值属性。
五、面试题:sizeof… 在什么时候用?
template <typename... Args>
void log(Args... args) {
// sizeof...(Args) 是编译期常量
if constexpr (sizeof...(Args) == 0) {
std::cout << "no args\n";
} else {
// 有参数的处理
}
}
面试官问:“sizeof… 能用在运行时吗?”
- 不能。
sizeof...(Args)是编译期求值的,等价于编译器数一数模板参数的数量,然后替换成常量。运行时不能得到这个值。