可变长参数模版
需要预先学习:
1. 可变长参数模版
普通模版的参数数量是固定的——template <typename T1, typename T2> 只能接受两个类型。而可变长参数模版(variadic template)打破了这一限制:可以写出接受任意数量、任意类型参数的模版。
这是 C++11 引入的特性,std::tuple、std::make_shared、std::thread 的构造函数等大量标准库组件都是基于它实现的。
1.1 基础语法
可变长参数模版的核心是参数包(parameter pack)。用 typename... Args 声明一个类型参数包,用 Args... args 声明一个函数参数包:
template <typename... Args>
void print(Args... args)
{
// args 是一个参数包,包含调用时传入的全部实参
}
Args 是一个类型参数包(type parameter pack),代表零个或多个类型;args 是一个函数参数包(function parameter pack),代表零个或多个函数实参。调用 print(1, 2.0, "hello") 时,Args 展开为 int, double, const char*,args 展开为 1, 2.0, "hello"。
1.2 包展开
包展开(pack expansion)的语法是 模式...——在参数包名后加 ...,编译器会把"模式"应用到参数包中的每个元素:
template <typename... Args>
void forward_all(Args... args)
{
// args... —— 将参数包原样展开
foo(args...); // 展开为 foo(a1, a2, a3)
// &args... —— 对每个参数取地址
foo(&args...); // 展开为 foo(&a1, &a2, &a3)
// f(args)... —— 对每个参数调用 f()
foo(args) // 展开为 foo(a1); foo(a2); foo(a3)
}
最常见的用法是在函数调用中展开参数包:
template <typename... Args>
void call_with_header(Args... args)
{
std::cout << "调用开始\n";
some_function(args...); // 将所有参数原封不动地转发
}
1.3 sizeof... 运算符
想知道参数包中有多少个参数,使用 sizeof... 运算符:
template <typename... Args>
void count(Args... args)
{
std::cout << "参数个数: " << sizeof...(Args) << '\n';
// 等价于 sizeof...(args)
}
int main()
{
count(); // 输出 0
count(1, 2, 3); // 输出 3
count("a", 3.14, 1); // 输出 3
}
1.4 递归展开
处理参数包的经典模式是递归展开——每次取出"第一个"参数,递归处理剩余参数,直到参数包为空时走终止条件。
#include <iostream>
// 终止条件:参数包为空时调用
void print_all()
{
std::cout << '\n';
}
// 递归:处理第一个参数,递归处理剩余
template <typename T, typename... Args>
void print_all(T first, Args... args)
{
std::cout << first << ' ';
print_all(args...); // 递归调用,参数包少了一个
}
int main()
{
print_all(42, 3.14, "hello", 'X');
}
展开过程:
| 调用 | 第一个参数 | 剩余参数 |
|---|---|---|
print_all(42, 3.14, "hello", 'X') |
42 |
3.14, "hello", 'X' |
print_all(3.14, "hello", 'X') |
3.14 |
"hello", 'X' |
print_all("hello", 'X') |
"hello" |
'X' |
print_all('X') |
'X' |
— |
print_all() |
— | —(终止) |
2. 折叠表达式(C++17)
递归展开虽然通用,但写法稍显啰嗦。C++17 引入了折叠表达式(fold expression),可以在一条语句中对参数包执行逐元运算。
2.1 四种折叠形式
| 语法 | 展开结果 | 名称 |
|---|---|---|
(... op args) |
(a1 op a2) op a3 |
一元左折叠 |
(args op ...) |
a1 op (a2 op a3) |
一元右折叠 |
(init op ... op args) |
((init op a1) op a2) op a3 |
二元左折叠 |
(args op ... op init) |
a1 op (a2 op (a3 op init)) |
二元右折叠 |
2.2 求和:从递归到折叠
用递归方式对参数包求和,需要写多个重载;用折叠表达式则一行搞定:
// 递归方式(C++11)
template <typename T>
T sum(T v)
{
return v;
}
template <typename T, typename... Args>
T sum(T first, Args... args)
{
return first + sum(args...);
}
// 折叠表达式
template <typename... Args>
auto sum_fold(Args... args)
{
return (... + args); // 一元左折叠
}
int main()
{
std::cout << sum_fold(1, 2, 3, 4, 5) << '\n'; // 输出 15
}
其它运算符同样适用—— (... && args) 判断全部为真, (... , args) 依次执行每个表达式:
template <typename... Args>
bool all_true(Args... args)
{
return (... && args);
}
template <typename F, typename... Args>
void for_each(F f, Args... args)
{
(f(args), ...); // 逗号折叠:对每个参数调用 f
}
int main()
{
std::cout << std::boolalpha;
std::cout << all_true(true, true, false) << '\n'; // 输出 false
for_each([](auto x) { std::cout << x << ' '; }, 1, 2, 3);
// 输出:1 2 3
}
3. 变参类模版
类模版同样可以使用可变长参数。最典型的例子是 std::tuple 的简化实现。
3.1 递归继承实现 Tuple
// 主模版:至少有一个元素
template <typename T, typename... Rest>
class Tuple : private Tuple<Rest...>
{
public:
Tuple(T v, Rest... rest)
: Tuple<Rest...>(rest...), m_value(v) {}
T head() const { return m_value; }
private:
T m_value;
};
// 特化:空参数包,作为递归终点
template <>
class Tuple<>
{
// 空——递归终止
};
int main()
{
Tuple<int, double, std::string> t(42, 3.14, "hello");
}
3.2 变参类模版的偏特化
处理变参类模版时,偏特化经常用来从参数包中"提取模式":
// 判断一个类型是否在类型列表中
template <typename Search, typename... Types>
struct Contains;
// 偏特化:参数包不为空,检查第一个
template <typename Search, typename Head, typename... Tail>
struct Contains<Search, Head, Tail...>
{
static constexpr bool value =
std::is_same_v<Search, Head> || Contains<Search, Tail...>::value;
};
// 偏特化:参数包为空,没找到
template <typename Search>
struct Contains<Search>
{
static constexpr bool value = false;
};
int main()
{
std::cout << Contains<int, double, int, char>::value << '\n'; // 输出 1 (true)
std::cout << Contains<float, double, int, char>::value << '\n'; // 输出 0 (false)
}
4. 实际应用
4.1 完美转发变参
结合 std::forward 和可变长参数模版,可以实现完美转发——将参数原样传递给另一个函数。
详见 -完美转发
例如一个 make_unique 的自定义实现,使用 std::forward<Args>(args)... 同时对每个参数进行完美转发:
#include <memory>
template <typename T, typename... Args>
std::unique_ptr<T> make_unique_custom(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
4.2 emplace 系列函数
std::vector::emplace_back 也是用可变长参数模版实现的——它接受任意数量和类型的参数,直接在容器内部构造对象,避免拷贝:
#include <vector>
#include <string>
int main()
{
std::vector<std::string> words;
words.emplace_back("hello"); // 构造 "hello"
words.emplace_back(5, 'x'); // 构造 "xxxxx"
words.emplace_back(words[0].begin(),
words[0].end()); // 从迭代器区间构造
for (auto& w : words)
std::cout << w << '\n';
// 输出:
// hello
// xxxxx
// hello
}


