0

0

C++17折叠表达式怎么用 简化可变参数模板技巧

P粉602998670

P粉602998670

发布时间:2025-08-26 11:42:02

|

1082人浏览过

|

来源于php中文网

原创

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

c++17折叠表达式怎么用 简化可变参数模板技巧

C++17的折叠表达式(Fold Expressions)是我个人认为语言在简化可变参数模板方面迈出的一大步,它把原本需要递归、辅助函数或者一些巧妙但晦涩的技巧才能完成的任务,浓缩成了一行简洁的代码。简单来说,它提供了一种在参数包上应用二元操作的优雅方式。

解决方案

折叠表达式的核心思想,就是将一个二元操作符(比如

+
,
-
,
*
,
/
,
&&
,
||
,
,
等)“折叠”到参数包中的所有元素上。这玩意儿极大地简化了可变参数模板的编写,让代码变得异常清晰。

想象一下,你有一堆数字,想把它们加起来。在C++17之前,你可能得写个递归函数:

template
auto sum(T t) {
    return t;
}

template
auto sum(T t, Rest... rest) {
    return t + sum(rest...);
}
// 调用 sum(1, 2, 3, 4)

而有了折叠表达式,这事儿就变得简单粗暴:

立即学习C++免费学习笔记(深入)”;

template
auto sum(Args... args) {
    return (args + ...); // Unary left fold: (((arg1 + arg2) + arg3) + ...)
}
// 调用 sum(1, 2, 3, 4)

是不是瞬间感觉清爽多了?

除了求和,它还能做很多事,比如打印:

#include 

template
void print(Args... args) {
    // 使用逗号运算符实现序列化打印
    // 注意:这里的逗号运算符是表达式逗号,它会按顺序执行左侧表达式并丢弃其结果,然后评估右侧表达式。
    // 整个折叠表达式的值是最后一个表达式的值,但我们主要利用其副作用(打印)。
    (std::cout << args << " ", ...); 
    std::cout << std::endl;
}
// 调用 print(1, "hello", 3.14) 会输出 "1 hello 3.14 "

或者进行逻辑判断:

template
bool all_true(Bools... b) {
    return (true && ... && b); // Binary left fold with initial value 'true'
}
// all_true(true, true, false) 返回 false

折叠表达式的强大之处在于,它将参数包的展开和操作结合在一起,省去了我们手动处理递归基线和中间状态的麻烦。它本质上就是编译器在编译期帮你把那个“递归”或者“循环”给展开了。

C++17折叠表达式解决了哪些痛点?

说实话,在我看来,折叠表达式的引入,主要解决了可变参数模板在处理“聚合操作”时冗余和晦涩的问题。过去,我们面对一个参数包,如果想对所有元素执行某个操作并聚合结果(比如求和、求积、逻辑与/或、字符串拼接),通常有几种选择:

  1. 递归函数/模板特化: 这是最常见的模式。你得写一个处理单个参数的基准模板,再写一个处理多个参数并递归调用自身的模板。这不仅代码量翻倍,而且对于初学者来说,理解其递归展开过程也需要一点时间。比如上面的
    sum
    例子,就是典型的递归模式。
  2. 逗号运算符技巧: 对于像打印这种不需要聚合最终结果,只需要按顺序执行副作用的操作,我们有时会利用逗号运算符的特性,结合初始化列表或者其他方式来“展开”参数包。这种方法虽然能在一行内完成,但语法上往往比较“黑魔法”,可读性不高,尤其是对不熟悉这种技巧的人来说。例如
    int arr[] = {(print(args), 0)...};
    这种写法,虽然能达到目的,但看起来总有点别扭。
  3. 辅助结构或宏: 有些更复杂的场景,可能需要引入额外的结构体、类或者宏来辅助处理参数包,以避免直接的递归。这无疑增加了设计的复杂性。

折叠表达式的出现,直接把这些“痛点”变成了“甜点”。它用一种声明式、直观的方式表达了“对参数包中的每个元素应用这个操作符”的意图。你不需要再考虑递归的基线是什么,也不用担心逗号运算符的副作用和优先级问题,编译器都帮你处理好了。代码变得更短、更清晰,也更不容易出错。它就像是给可变参数模板加了一个“一键聚合”的功能,极大地提升了开发效率和代码的可维护性。

折叠表达式的四种基本形式及应用场景是什么?

折叠表达式实际上有四种基本形式,它们在语法和行为上略有不同,但都围绕着一个核心:如何将一个二元操作符应用于参数包。理解这四种形式,能帮助你更灵活地运用它。

  1. 一元左折叠 (Unary Left Fold):

    (... op pack)

    • 语法:
      (... op pack)
    • 展开方式:
      ((pack_1 op pack_2) op pack_3) ... op pack_N
    • 特点: 从左到右结合,没有初始值。如果参数包为空,则会导致编译错误
    • 应用场景: 适用于需要从左到右累积计算的场景,例如求和
      (args + ...)
      、求积
      (args * ...)
      、逻辑与
      (args && ...)
    • 示例:
      template
      auto product(Nums... nums) {
          return (nums * ...); // (n1 * n2) * n3 ...
      }
      // product(2, 3, 4) -> (2*3)*4 = 24
  2. 一元右折叠 (Unary Right Fold):

    (pack op ...)

    • 语法:
      (pack op ...)
    • 展开方式:
      pack_1 op (pack_2 op (... op pack_N))
    • 特点: 从右到左结合,没有初始值。如果参数包为空,则会导致编译错误。
    • 应用场景: 相对较少,但对于某些右结合的操作符或特定算法可能有用,例如函数链式调用或者某些自定义类型操作。
    • 示例:
      template
      void call_in_order_right_to_left(Funcs... funcs) {
          // 假设funcs是可调用对象,这里用逗号运算符实现右结合调用
          (funcs(), ...); // func1(), (func2(), (... funcN()))
          // 实际效果是func1先执行,然后是func2,以此类推。
          // 但如果操作符是其他右结合的,比如自定义的>>操作符,就会体现出右结合的特性。
      }

      请注意,对于逗号运算符,无论是左折叠还是右折叠,其执行顺序都是从左到右。这里主要展示的是语法形式。

  3. 二元左折叠 (Binary Left Fold):

    (init op ... op pack)

    Moshi Chat
    Moshi Chat

    法国AI实验室Kyutai推出的端到端实时多模态AI语音模型,具备听、说、看的能力,不仅可以实时收听,还能进行自然对话。

    下载
    • 语法:
      (init op ... op pack)
    • 展开方式:
      (((init op pack_1) op pack_2) op pack_3) ... op pack_N
    • 特点: 从左到右结合,有一个初始值
      init
      。即使参数包为空,表达式也能计算出结果(即
      init
      的值)。
    • 应用场景: 这是最常用的一种形式,尤其适合需要一个累积起始值的操作。
    • 示例:
      template
      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 (当参数包为空时)
  4. 二元右折叠 (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
      bool is_less_than_all(T val, Args... args) {
          return (val < ... < args); // val < (arg1 < (arg2 < ...))
          // 这是一个链式比较的例子,但实际行为依赖于操作符的定义。
          // 对于内置的`<`,它不是链式比较,而是先计算右侧,再用val与结果比较。
          // 真正有用的场景可能是自定义的右结合操作符。
      }
      
      // 举个更实际的例子,用逗号运算符实现从右到左的初始化
      template
      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, ...)); // 从右到左组合函数
      }

      二元右折叠在处理函数组合、管道操作(如果操作符设计得当)时能展现出其优势。

理解这四种形式的关键在于“初始值”的存在与否,以及“结合方向”。它们为处理可变参数模板提供了极大的灵活性和表达力。

除了简化求和,折叠表达式还能实现哪些高级技巧?

折叠表达式的威力远不止于简单的求和或打印。它能深入到更复杂的类型操作、函数调用、甚至编译期检查中,极大地提升了可变参数模板的实用性和简洁性。

  1. 构建异构容器或元组: 一个常见的需求是,将参数包中的元素直接“塞进”一个

    std::tuple
    或其他异构容器。有了折叠表达式,这变得非常直接:

    #include 
    #include 
    
    template
    auto make_tuple_from_pack(Args&&... args) {
        // 使用逗号运算符和std::forward,将所有参数完美转发到tuple的构造函数中
        // 实际上,std::make_tuple已经做了类似的事情,这里只是展示折叠表达式的能力
        return std::tuple(std::forward(args)...); // 这是tuple本身的构造,不是折叠表达式直接构建
        // 更好的例子是,如果你想在构建tuple时对每个元素做一些预处理:
        // return std::make_tuple((process(args))...); // 假设process是个函数
    }
    // 尽管如此,直接构造std::tuple(args...) 已经很简洁。
    // 折叠表达式在构建容器时,更多体现在对每个元素进行操作后收集结果,例如:
    template
    std::vector get_lengths(const Args&... args) {
        std::vector 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}

    这个例子展示了如何利用逗号运算符的副作用,将参数包中的元素逐一处理并添加到容器中,而不需要显式的循环或递归。

  2. 通用函数调用与转发: 当需要对参数包中的每个元素执行一个函数,或者将参数包转发给另一个函数时,折叠表达式能提供非常简洁的方案。

    #include  // For std::invoke
    
    template
    void apply_to_each(Func f, Args&&... args) {
        // 使用逗号运算符,对每个参数调用函数f
        // std::invoke 确保了成员函数指针、普通函数指针、lambda等都能正确调用
        (std::invoke(f, std::forward(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
    void apply_member_to_each(Func f, T& obj, Args&&... args) {
        (std::invoke(f, obj, std::forward(args)), ...);
    }
    // MyClass obj;
    // apply_member_to_each(&MyClass::print_member, obj, 10, 20);

    这比手动循环或者递归调用要清晰得多。

  3. 编译期类型检查与属性聚合: 结合

    std::is_same_v
    std::is_convertible_v
    等类型特征,折叠表达式可以在编译期对参数包的类型进行检查或聚合其属性。

    #include  // For std::is_integral_v
    
    template
    constexpr bool all_are_integral() {
        // 检查参数包中所有类型是否都是整型
        return (std::is_integral_v && ...); 
    }
    
    // static_assert(all_are_integral()); // 编译通过
    // static_assert(all_are_integral());   // 编译失败,因为double不是整型

    这种方式在编写通用模板库时非常有用,可以用于静态断言,确保模板参数符合预期。

  4. 实现自定义的“管道”操作符: 虽然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
    auto pipe(T initial_value, Funcs... funcs) {
        // 这是一个二元左折叠,初始值是initial_value,操作符是函数调用
        // 这里的f(val)是自定义的“操作符”,实际是lambda
        return (initial_value | ... | funcs); // 语法错误,不能直接用 | 模拟函数调用
        // 正确的实现需要一个辅助的lambda或者操作符重载
    }
    
    // 更实际的实现可能是这样:
    template
    T apply_func(T val, Func f) {
        return f(val);
    }
    
    template
    auto pipe_chain(T initial_value, Funcs... funcs) {
        // 使用二元左折叠,每次将当前值和下一个函数传递给apply_func
        return (initial_value | ... | [](auto val, auto f){ return f(val); }); // 语法错误,lambda不能直接作为操作符
        // 实际应用中,通常会利用操作符重载或更复杂的技巧。
        // 最直接的模拟是利用逗号运算符的副作用,但那不是“管道”
    }
    
    // 一个简单的链式调用模拟:
    template
    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++在处理可变参数模板时的表达能力和代码简洁性。它不仅仅是语法糖,更是对编译器优化能力的释放,让开发者能以更声明式的方式编写高度泛化的代码。

相关专题

更多
python中print函数的用法
python中print函数的用法

python中print函数的语法是“print(value1, value2, ..., sep=' ', end=' ', file=sys.stdout, flush=False)”。本专题为大家提供print相关的文章、下载、课程内容,供大家免费下载体验。

183

2023.09.27

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1435

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

223

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

84

2025.10.17

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

248

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

205

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1435

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

609

2023.11.24

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

7

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
10分钟--Midjourney创作自己的漫画
10分钟--Midjourney创作自己的漫画

共1课时 | 0.1万人学习

Midjourney 关键词系列整合
Midjourney 关键词系列整合

共13课时 | 0.9万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号