可变参数模板
定义与作用
可变参数模板(Variadic Templates,C++11)允许模板接受任意数量的模板参数:
template <typename... Args>
void print(Args... args); // Args 是类型参数包,args 是函数参数包
它终结了"为每个参数数量写一个重载"的噩梦,是 std::tuple、std::make_shared、std::thread 等标准库组件的核心机制。
核心原理
参数包展开模式
递归展开 vs 折叠表达式
完整示例
示例一:通用打印函数——递归展开
场景说明:实现一个类似 Python print 的泛型打印函数,支持任意类型和数量。
#include <iostream>
#include <string>
#include <sstream>
// ====== C++11 递归展开 ======
// 终止条件:无参数时什么都不做
void format_print(std::ostream& os) {}
// 递归展开:每次处理第一个参数
template <typename T, typename... Args>
void format_print(std::ostream& os, const T& first, const Args&... rest) {
os << first;
if constexpr (sizeof...(rest) > 0) {
os << ", ";
}
format_print(os, rest...);
}
// 包装函数
template <typename... Args>
void println(const Args&... args) {
format_print(std::cout, args...);
std::cout << std::endl;
}
// ====== 求和函数:递归展开 ======
// 终止条件
template <typename T>
T variadic_sum(T last) {
return last;
}
template <typename T, typename... Args>
T variadic_sum(T first, Args... rest) {
return first + variadic_sum(rest...);
}
// ====== 拼接字符串——C++17 折叠表达式 ======
template <typename... Args>
std::string concat(const Args&... args) {
std::ostringstream oss;
(oss << ... << args); // 二元左折叠:(((oss << a1) << a2) << a3)
return oss.str();
}
// ====== 全真判断 ======
template <typename... Args>
bool all_true(Args... args) {
return (... && args); // 一元右折叠:a1 && (a2 && (a3 && true))
}
int main() {
std::cout << "===== 泛型打印 =====" << std::endl;
println("Hello", 42, 3.14, std::string("World"));
println("学生:", "张三", "分数:", 95);
std::cout << "\n===== 求和 =====" << std::endl;
std::cout << "sum(1,2,3,4,5) = " << variadic_sum(1, 2, 3, 4, 5) << std::endl;
std::cout << "sum(1.1, 2.2, 3.3) = " << variadic_sum(1.1, 2.2, 3.3) << std::endl;
std::cout << "\n===== 折叠表达式 =====" << std::endl;
std::cout << "concat: " << concat("C++", 17, " is ", "modern") << std::endl;
std::cout << "all_true(true, true, false, true): "
<< std::boolalpha << all_true(true, true, false, true) << std::endl;
std::cout << "all_true(true, true, true): "
<< all_true(true, true, true) << std::endl;
return 0;
}
预期输出:
===== 泛型打印 =====
Hello, 42, 3.14, World
学生:, 张三, 分数:, 95
===== 求和 =====
sum(1,2,3,4,5) = 15
sum(1.1, 2.2, 3.3) = 6.6
===== 折叠表达式 =====
concat: C++17 is modern
all_true(true, true, false, true): false
all_true(true, true, true): true
逐段分析:
- 递归展开(C++11):
format_print每次处理参数包的第一个参数first,剩余参数rest...递归调用 - 终止条件:空参数版本的
format_print(os)是递归基(base case) sizeof...(rest)在编译期返回参数包剩余参数数量- 折叠表达式(C++17):
(... && args)等价于a1 && a2 && a3 && ...,代码量从递归的 3 个函数减少到 1 行
示例二:简易 Tuple 实现
#include <iostream>
#include <string>
#include <utility>
// ====== 简易 Tuple 实现 ======
// 主模板声明
template <typename... Types>
class Tuple;
// 递归基:空 Tuple
template <>
class Tuple<> {
public:
static constexpr size_t size = 0;
};
// 递归定义:Tuple<Head, Tail...>
template <typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
public:
Tuple() = default;
Tuple(Head head, Tail... tail)
: head_(std::move(head)), Tuple<Tail...>(std::move(tail)...) {}
Head& head() { return head_; }
const Head& head() const { return head_; }
Tuple<Tail...>& tail() {
return *this; // 向上转型为基类
}
const Tuple<Tail...>& tail() const {
return *this;
}
static constexpr size_t size = 1 + sizeof...(Tail);
private:
Head head_;
};
// ====== 按索引获取元素 ======
template <size_t Index, typename TupleType>
struct TupleElement;
// 索引 0:取当前 Tuple 的 head
template <typename Head, typename... Tail>
struct TupleElement<0, Tuple<Head, Tail...>> {
using type = Head;
static Head& get(Tuple<Head, Tail...>& t) { return t.head(); }
static const Head& get(const Tuple<Head, Tail...>& t) { return t.head(); }
};
// 索引 N > 0:递归到 tail
template <size_t Index, typename Head, typename... Tail>
struct TupleElement<Index, Tuple<Head, Tail...>> {
using type = typename TupleElement<Index - 1, Tuple<Tail...>>::type;
static type& get(Tuple<Head, Tail...>& t) {
return TupleElement<Index - 1, Tuple<Tail...>>::get(t.tail());
}
static const type& get(const Tuple<Head, Tail...>& t) {
return TupleElement<Index - 1, Tuple<Tail...>>::get(t.tail());
}
};
// 获取函数
template <size_t Index, typename... Types>
auto get(Tuple<Types...>& t)
-> typename TupleElement<Index, Tuple<Types...>>::type& {
return TupleElement<Index, Tuple<Types...>>::get(t);
}
int main() {
std::cout << "===== 简易 Tuple =====" << std::endl;
Tuple<int, std::string, double> t(42, "Hello", 3.14);
std::cout << "Tuple size: " << t.size << std::endl;
std::cout << "get<0>: " << get<0>(t) << std::endl;
std::cout << "get<1>: " << get<1>(t) << std::endl;
std::cout << "get<2>: " << get<2>(t) << std::endl;
return 0;
}
预期输出:
===== 简易 Tuple =====
Tuple size: 3
get<0>: 42
get<1>: Hello
get<2>: 3.14
逐段分析:
- Tuple 通过递归继承实现:
Tuple<Head, Tail...>继承自Tuple<Tail...>,层层存储 head - 空
Tuple<>是递归基类 TupleElement<Index, Tuple>通过偏特化递归剥离类型来获取指定索引的元素- 这是
std::tuple的简化实现原理——展示可变参数模板的核心应用
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 递归展开缺少终止条件 | 编译期无限递归 | 必须提供 0 或 1 参数的重载作为递归基 |
| 参数包展开位置错误 | 语法错误(args... vs args ...) | ... 紧跟参数包名 |
| 折叠表达式括号缺失 | 运算符优先级问题 | 折叠表达式整体加括号 (args + ...) |
| 空参数包调用 | 少数情况需要额外处理 | 提供空参数的函数重载 |
sizeof...(Args) 用于运行时 | 编译期常量无法直接用于运行时 | 用 if constexpr 或模板特化分流 |
常见面试问题
可变参数模板的展开方式有哪些?——(1) 递归展开(C++11);(2) 折叠表达式(C++17);(3) 初始化列表展开
int arr[] = { (func(args), 0)... };;(4) 逗号表达式展开。折叠表达式的四种形式?——一元右折叠
(pack op ...)、一元左折叠(... op pack)、二元右折叠(pack op ... op init)、二元左折叠(init op ... op pack)。参数包的 sizeof... 是什么?——编译期返回参数包中元素数量的常量。
sizeof...(Args)返回类型参数个数,sizeof...(args)返回函数参数个数。std::tuple的实现原理?——通过可变参数模板 + 递归继承实现。每个实例存储一个元素并继承剩余元素的 Tuple。完美转发结合可变参数模板怎么写?——
template<typename... Args> void f(Args&&... args) { g(std::forward<Args>(args)...); }。
小结
- 可变参数模板(C++11)接受任意数量的模板参数——参数包用
...声明 - 递归展开是最基本的处理方式——必须提供递归终止条件
- 折叠表达式(C++17)大幅简化参数包操作——一元/二元、左折叠/右折叠
std::tuple通过递归继承实现——可变参数模板的最经典应用- 与完美转发结合:
std::forward<Args>(args)...是 C++ 中最强大的转发机制