变参模板与折叠表达式
使用参数包处理任意数量的模板参数。掌握折叠表达式以优雅地、无需递归地处理包。
参数包
变参模板使用参数包接受任意数量的模板参数。语法使用省略号(...)。有两种包:
1. 模板参数包 ── template ── 捕获零个或多个类型参数。
2. 函数参数包 ── void f(Ts... args) ── 捕获对应的函数参数。
你不能直接在循环中遍历包。相反,你使用 ... 运算符来展开它。关键要点是包展开将一个模式应用于包的每个元素,并生成一个逗号分隔的列表。
sizeof...(Ts) 和 sizeof...(args) 返回包中元素的数量,类型为 constexpr std::size_t。
递归包展开(旧模式)
在 C++17 折叠表达式之前,处理包的标准方式是递归实例化:剥离第一个元素,处理它,然后对剩余部分递归。这需要一个基本情况来终止递归。
#include <iostream>
// Base case: no arguments
void print() {
std::cout << '\n';
}
// Recursive case: process first, recurse on rest
template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print(rest...); // expand rest, recurse
}
int main() {
print(1, 2.5, "hello", 'A');
// Output: 1, 2.5, hello, A
print(); // just a newline
}折叠表达式(C++17)
C++17 折叠表达式在大多数情况下消除了对递归模板的需求。折叠表达式将一个二元运算符应用于参数包的所有元素。有四种形式:
- 一元右折叠:(pack op ...) 展开为 e1 op (e2 op (... op eN))
- 一元左折叠:(... op pack) 展开为 ((e1 op e2) op ...) op eN
- 二元右折叠:(pack op ... op init) ── 包含初始值
- 二元左折叠:(init op ... op pack) ── 包含初始值
#include <iostream>
#include <string>
// Sum all arguments — unary left fold with +
template <typename... Ts>
auto sum(Ts... args) {
return (... + args); // ((a1 + a2) + a3) + ...
}
// Print all arguments — binary left fold with <<
template <typename... Ts>
void print(Ts&&... args) {
(std::cout << ... << args) << '\n';
// expands to: ((std::cout << a1) << a2) << a3 ...
}
// Check if ALL arguments satisfy a predicate — unary left fold with &&
template <typename... Ts>
bool all_positive(Ts... args) {
return (... && (args > 0)); // (a1>0) && (a2>0) && ...
}
// Push multiple elements to a vector — fold over comma operator
#include <vector>
template <typename T, typename... Ts>
void push_all(std::vector<T>& vec, Ts&&... args) {
(vec.push_back(std::forward<Ts>(args)), ...);
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << '\n'; // 15
print("x=", 10, " y=", 20); // x=10 y=20
std::cout << std::boolalpha
<< all_positive(1, 2, 3) << '\n' // true
<< all_positive(1, -2, 3) << '\n'; // false
std::vector<int> v;
push_all(v, 10, 20, 30);
for (int x : v) std::cout << x << ' '; // 10 20 30
std::cout << '\n';
}包展开上下文
包展开(...)可以出现在许多上下文中,不仅限于函数调用参数:
- 函数参数:f(args...) 或 f(transform(args)...)
- 模板参数:std::tuple
- 基类:class Derived : public Bases... { };
- 初始化列表:auto list = { args... };
- Lambda 捕获:[args...] { } 或 [...args = std::move(args)] { }(C++20 初始化捕获)
- Using 声明(C++17):using Bases::operator()...;
模式是 ... 之前出现的内容。在 f(transform(args)...) 中,模式是 transform(args),它展开为 f(transform(a1), transform(a2), transform(a3))。
理解这一点至关重要:f(args)... 与 f(args...) 不同。前者展开为 f(a1), f(a2), f(a3)(逗号表达式中的三次独立调用),而后者展开为 f(a1, a2, a3)(一次调用带三个参数)。
std::tuple 与 std::apply
std::tuple 是标准库的异构容器,建立在变参模板之上。std::apply(C++17)将元组元素解包为参数来调用可调用对象。std::index_sequence 支持对元组进行编译时索引。
#include <tuple>
#include <iostream>
#include <string>
#include <utility>
// std::apply calls a function with tuple elements as arguments
void greet(const std::string& name, int age) {
std::cout << "Hello " << name << ", age " << age << '\n';
}
// Print all tuple elements using index_sequence
template <typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
std::cout << '(';
print_tuple_impl(t, std::index_sequence_for<Ts...>{});
std::cout << ")\n";
}
int main() {
auto person = std::make_tuple(std::string("Alice"), 30);
std::apply(greet, person); // Hello Alice, age 30
auto data = std::make_tuple(1, 3.14, std::string("hello"));
print_tuple(data); // (1, 3.14, hello)
}变参模板最常见的错误:
1. 忘记 ... ── 写 args 而不是 args... 会导致包未展开,产生编译错误。
2. 在错误的位置展开 ── f(g(args)...) 与 f(g(args...)) 含义非常不同。前者为每个元素调用一次 g;后者用所有元素调用一次 g。
3. 空包 ── (... + args) 对于空包在大多数运算符下是格式错误的。使用带初始值的二元折叠:(0 + ... + args)。
4. sizeof... 返回的是计数,不是字节大小 ── sizeof...(args) 给出包元素的数量,而不是它们的合并内存大小。
尽可能优先使用折叠表达式而非递归变参模板。它们更简洁、更易读,且通常编译更快。使用带显式初始值的二元折叠来安全处理空包。将递归展开保留给折叠表达式不够用的场景 ── 例如,当你需要成对处理元素时,或者当处理每个元素需要不同的逻辑时。
- 变参模板使用
typename... Ts(模板参数包)和Ts... args(函数参数包) sizeof...(args)在编译时返回包中元素的数量- 包展开将模式应用于每个元素:
f(transform(args)...)对每个元素调用 transform - 折叠表达式(C++17)取代递归展开:
(... + args)对所有元素求和 - 使用带初始值的二元折叠来安全处理空包
std::apply将元组解包为函数参数;std::index_sequence支持编译时索引
测验 — 检验你的知识
(20 经验值)1. 对于 `args = {a, b, c}`,`(... + args)` 展开为什么?
2. `f(args...)` 和 `f(args)...` 之间有什么区别?
3. `sizeof...(args)` 返回什么?