折叠表达式通过四种形式(一元/二元左/右折叠)简化可变参数模板,支持求和、打印、逻辑判断等聚合操作,避免递归和晦涩技巧,提升代码清晰度与编译期处理能力。

C++17的折叠表达式(Fold Expressions)是我个人认为语言在简化可变参数模板方面迈出的一大步,它把原本需要递归、辅助函数或者一些巧妙但晦涩的技巧才能完成的任务,浓缩成了一行简洁的代码。简单来说,它提供了一种在参数包上应用二元操作的优雅方式。
折叠表达式的核心思想,就是将一个二元操作符(比如
+
-
*
/
&&
||
,
想象一下,你有一堆数字,想把它们加起来。在C++17之前,你可能得写个递归函数:
template<typename T>
auto sum(T t) {
return t;
}
template<typename T, typename... Rest>
auto sum(T t, Rest... rest) {
return t + sum(rest...);
}
// 调用 sum(1, 2, 3, 4)而有了折叠表达式,这事儿就变得简单粗暴:
立即学习“C++免费学习笔记(深入)”;
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // Unary left fold: (((arg1 + arg2) + arg3) + ...)
}
// 调用 sum(1, 2, 3, 4)是不是瞬间感觉清爽多了?
除了求和,它还能做很多事,比如打印:
#include <iostream>
template<typename... Args>
void print(Args... args) {
// 使用逗号运算符实现序列化打印
// 注意:这里的逗号运算符是表达式逗号,它会按顺序执行左侧表达式并丢弃其结果,然后评估右侧表达式。
// 整个折叠表达式的值是最后一个表达式的值,但我们主要利用其副作用(打印)。
(std::cout << args << " ", ...);
std::cout << std::endl;
}
// 调用 print(1, "hello", 3.14) 会输出 "1 hello 3.14 "或者进行逻辑判断:
template<typename... Bools>
bool all_true(Bools... b) {
return (true && ... && b); // Binary left fold with initial value 'true'
}
// all_true(true, true, false) 返回 false折叠表达式的强大之处在于,它将参数包的展开和操作结合在一起,省去了我们手动处理递归基线和中间状态的麻烦。它本质上就是编译器在编译期帮你把那个“递归”或者“循环”给展开了。
说实话,在我看来,折叠表达式的引入,主要解决了可变参数模板在处理“聚合操作”时冗余和晦涩的问题。过去,我们面对一个参数包,如果想对所有元素执行某个操作并聚合结果(比如求和、求积、逻辑与/或、字符串拼接),通常有几种选择:
sum
int arr[] = {(print(args), 0)...};折叠表达式的出现,直接把这些“痛点”变成了“甜点”。它用一种声明式、直观的方式表达了“对参数包中的每个元素应用这个操作符”的意图。你不需要再考虑递归的基线是什么,也不用担心逗号运算符的副作用和优先级问题,编译器都帮你处理好了。代码变得更短、更清晰,也更不容易出错。它就像是给可变参数模板加了一个“一键聚合”的功能,极大地提升了开发效率和代码的可维护性。
折叠表达式实际上有四种基本形式,它们在语法和行为上略有不同,但都围绕着一个核心:如何将一个二元操作符应用于参数包。理解这四种形式,能帮助你更灵活地运用它。
一元左折叠 (Unary Left Fold): (... op pack)
(... op pack)
((pack_1 op pack_2) op pack_3) ... op pack_N
(args + ...)
(args * ...)
(args && ...)
template<typename... Nums>
auto product(Nums... nums) {
return (nums * ...); // (n1 * n2) * n3 ...
}
// product(2, 3, 4) -> (2*3)*4 = 24一元右折叠 (Unary Right Fold): (pack op ...)
(pack op ...)
pack_1 op (pack_2 op (... op pack_N))
template<typename... Funcs>
void call_in_order_right_to_left(Funcs... funcs) {
// 假设funcs是可调用对象,这里用逗号运算符实现右结合调用
(funcs(), ...); // func1(), (func2(), (... funcN()))
// 实际效果是func1先执行,然后是func2,以此类推。
// 但如果操作符是其他右结合的,比如自定义的>>操作符,就会体现出右结合的特性。
}请注意,对于逗号运算符,无论是左折叠还是右折叠,其执行顺序都是从左到右。这里主要展示的是语法形式。
二元左折叠 (Binary Left Fold): (init op ... op pack)
(init op ... op pack)
(((init op pack_1) op pack_2) op pack_3) ... op pack_N
init
init
template<typename... Args>
auto sum_with_initial(int initial_value, Args... args) {
return (initial_value + ... + args); // ((((initial_value + arg1) + arg2) + ...)
}
// sum_with_initial(10, 1, 2, 3) -> 10 + 1 + 2 + 3 = 16
// sum_with_initial(10) -> 10 (当参数包为空时)二元右折叠 (Binary Right Fold): (pack op ... op init)
语法:
(pack op ... op init)
展开方式:
pack_1 op (pack_2 op (... op (pack_N op init)))
特点: 从右到左结合,有一个初始值
init
init
应用场景: 对于需要从右向左处理的场景,比如链式比较或某些函数组合。
示例:
template<typename T, typename... Args>
bool is_less_than_all(T val, Args... args) {
return (val < ... < args); // val < (arg1 < (arg2 < ...))
// 这是一个链式比较的例子,但实际行为依赖于操作符的定义。
// 对于内置的`<`,它不是链式比较,而是先计算右侧,再用val与结果比较。
// 真正有用的场景可能是自定义的右结合操作符。
}
// 举个更实际的例子,用逗号运算符实现从右到左的初始化
template<typename T, typename... Values>
void assign_from_right(T& target, Values... vals) {
(target = vals, ...); // target = val1, (target = val2, ...)
// 实际赋值顺序是 val1, val2, ...。但如果用 (vals = target, ...),则会是 valN = target, ... val1 = target
// 这里更恰当的例子是函数组合:
auto compose = [](auto f, auto g){ return [=](auto x){ return f(g(x)); }; };
auto f_composed = (compose(funcs, ...)); // 从右到左组合函数
}二元右折叠在处理函数组合、管道操作(如果操作符设计得当)时能展现出其优势。
理解这四种形式的关键在于“初始值”的存在与否,以及“结合方向”。它们为处理可变参数模板提供了极大的灵活性和表达力。
折叠表达式的威力远不止于简单的求和或打印。它能深入到更复杂的类型操作、函数调用、甚至编译期检查中,极大地提升了可变参数模板的实用性和简洁性。
构建异构容器或元组: 一个常见的需求是,将参数包中的元素直接“塞进”一个
std::tuple
#include <tuple>
#include <string>
template<typename... Args>
auto make_tuple_from_pack(Args&&... args) {
// 使用逗号运算符和std::forward,将所有参数完美转发到tuple的构造函数中
// 实际上,std::make_tuple已经做了类似的事情,这里只是展示折叠表达式的能力
return std::tuple<Args...>(std::forward<Args>(args)...); // 这是tuple本身的构造,不是折叠表达式直接构建
// 更好的例子是,如果你想在构建tuple时对每个元素做一些预处理:
// return std::make_tuple((process(args))...); // 假设process是个函数
}
// 尽管如此,直接构造std::tuple<Args...>(args...) 已经很简洁。
// 折叠表达式在构建容器时,更多体现在对每个元素进行操作后收集结果,例如:
template<typename... Args>
std::vector<int> get_lengths(const Args&... args) {
std::vector<int> lengths;
// 这里的逗号运算符折叠,每次push_back一个元素的长度
(lengths.push_back(args.length()), ...);
return lengths;
}
// std::string s1 = "hello", s2 = "world";
// auto len_vec = get_lengths(s1, s2); // len_vec = {5, 5}这个例子展示了如何利用逗号运算符的副作用,将参数包中的元素逐一处理并添加到容器中,而不需要显式的循环或递归。
通用函数调用与转发: 当需要对参数包中的每个元素执行一个函数,或者将参数包转发给另一个函数时,折叠表达式能提供非常简洁的方案。
#include <functional> // For std::invoke
template<typename Func, typename... Args>
void apply_to_each(Func f, Args&&... args) {
// 使用逗号运算符,对每个参数调用函数f
// std::invoke 确保了成员函数指针、普通函数指针、lambda等都能正确调用
(std::invoke(f, std::forward<Args>(args)), ...);
}
// 示例:
void print_val(int x) { std::cout << "Val: " << x << std::endl; }
struct MyClass {
void print_member(int x) { std::cout << "Member Val: " << x << std::endl; }
};
// apply_to_each(print_val, 1, 2, 3);
// MyClass obj;
// apply_to_each(&MyClass::print_member, &obj, 10, 20); // 错误:成员函数需要对象实例
// 应该这样写:
template<typename Func, typename T, typename... Args>
void apply_member_to_each(Func f, T& obj, Args&&... args) {
(std::invoke(f, obj, std::forward<Args>(args)), ...);
}
// MyClass obj;
// apply_member_to_each(&MyClass::print_member, obj, 10, 20);这比手动循环或者递归调用要清晰得多。
编译期类型检查与属性聚合: 结合
std::is_same_v
std::is_convertible_v
#include <type_traits> // For std::is_integral_v
template<typename... T>
constexpr bool all_are_integral() {
// 检查参数包中所有类型是否都是整型
return (std::is_integral_v<T> && ...);
}
// static_assert(all_are_integral<int, long, short>()); // 编译通过
// static_assert(all_are_integral<int, double>()); // 编译失败,因为double不是整型这种方式在编写通用模板库时非常有用,可以用于静态断言,确保模板参数符合预期。
实现自定义的“管道”操作符: 虽然C++没有内置的管道操作符(
|>
// 假设我们有这样的函数:
auto add_one = [](int x){ return x + 1; };
auto multiply_two = [](int x){ return x * 2; };
auto subtract_three = [](int x){ return x - 3; };
// 我们可以设计一个“管道”辅助函数
template<typename T, typename... Funcs>
auto pipe(T initial_value, Funcs... funcs) {
// 这是一个二元左折叠,初始值是initial_value,操作符是函数调用
// 这里的f(val)是自定义的“操作符”,实际是lambda
return (initial_value | ... | funcs); // 语法错误,不能直接用 | 模拟函数调用
// 正确的实现需要一个辅助的lambda或者操作符重载
}
// 更实际的实现可能是这样:
template<typename T, typename Func>
T apply_func(T val, Func f) {
return f(val);
}
template<typename T, typename... Funcs>
auto pipe_chain(T initial_value, Funcs... funcs) {
// 使用二元左折叠,每次将当前值和下一个函数传递给apply_func
return (initial_value | ... | [](auto val, auto f){ return f(val); }); // 语法错误,lambda不能直接作为操作符
// 实际应用中,通常会利用操作符重载或更复杂的技巧。
// 最直接的模拟是利用逗号运算符的副作用,但那不是“管道”
}
// 一个简单的链式调用模拟:
template<typename T, typename... Funcs>
T chain_calls(T val, Funcs... funcs) {
// 依次将val传递给每个函数,并更新val
// 这是二元左折叠的经典应用,其中操作符是 lambda 表达式
return (((val = funcs(val)), ...), val);
// 解释: (val = f1(val)), (val = f2(val)), ...
// 整个表达式的结果是最后一个逗号表达式的值,也就是最终的val
}
// int result = chain_calls(5, add_one, multiply_two, subtract_three);
// 5 -> 6 -> 12 -> 9
// std::cout << result << std::endl; // 输出 9这个
chain_calls
总的来说,折叠表达式极大地提升了C++在处理可变参数模板时的表达能力和代码简洁性。它不仅仅是语法糖,更是对编译器优化能力的释放,让开发者能以更声明式的方式编写高度泛化的代码。
以上就是C++17折叠表达式怎么用 简化可变参数模板技巧的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号