别再写递归模板了!C++17 折叠表达式让你告别模板地狱!

开发
现代 C++ 的核心思想就是让复杂的事情变得简单,让危险的操作变得安全。折叠表达式就是最好的例子!

"啊啊啊!" 小王一头栽在键盘上,发出哀嚎,"这个可变参数模板要写吐了!" 🤮

老张正在享受他的下午茶时光,听到动静抬头一看,不禁莞尔。"又在折腾什么呢,小伙子?"

模板地狱初体验

"老张你看," 小王指着屏幕上密密麻麻的代码,"就是想计算几个数的和,写得我头晕眼花..."

// 基础情况 - 只有一个参数时的处理 🛑
template<typename T>
T sum(T v) {
    return v;  // 递归的终止条件
}

这是递归的基础情况,就像爬楼梯要有第一级台阶一样。。

接下来是递归的主体部分:

// 递归情况 - 处理多个参数 🔁
template<typename T, typename... Args>
T sum(T first, Args... args) {
    return first + sum(args...);  // 一层层往下递归 😵💫
}

这种写法就像套娃一样,一个函数调用套着另一个函数调用...

"哎呀," 老张喝了口咖啡,眼睛里闪着狡黠的光,"现在都2023年了,还在用这么老土的写法啊?"

"啊?" 小王一脸茫然

"来来来,看看新时代的写法!" 老张拉过键盘,手指飞快地敲击着:

template<typename... Args>
auto sum(Args... args) {
    return (... + args);  // 一行解决战斗! ⚔️
}

"这...这也行?" 小王目瞪口呆,"这简直就是魔法啊!" 

为什么折叠表达式更好? 

老张放下咖啡杯,开始细致地解释: "让我告诉你为什么新版本更优秀:"

  • 代码简洁度 📝 "看看原来的版本,需要两个模板函数,而且还要写递归。新版本只需要一个函数,一行代码就搞定!"
  • 编译效率 ⚡ "递归版本每处理一个参数都要生成一次函数调用,而折叠表达式在编译期就能展开成一个扁平的表达式。比如:"
sum(1, 2, 3, 4)  
// 递归版本展开:
1 + sum(2, 3, 4)
1 + (2 + sum(3, 4))
1 + (2 + (3 + sum(4)))
1 + (2 + (3 + 4))

// 折叠表达式直接展开:
((1 + 2) + 3) + 4

运行时性能 🚀 "递归版本每个递归调用都会产生函数调用开销,而折叠表达式会被编译器优化成一组简单的加法运算。"

小王若有所思地点点头,"原来如此!不仅代码更优雅,性能也更好!"

"不止这些呢!" 老张兴致勃勃地打开画图软件,"折叠表达式就像叠千纸鹤,有四种基本手法..."

折叠表达式四种武功

"等等,老张!" 小王挠挠头,"你说折叠表达式有四种手法,能具体讲讲吗?" 🤔

"当然!" 老张露出高深莫测的笑容,"我来给你演示一下:"

(1) 一元右折叠 (向右展开)

template<typename... Args>
void print_right(Args... args) {
    // 从右向左展开: a1 + (a2 + (a3 + a4)) 🔄
    (std::cout << ... << args)  // 从右向左展开
}

"就像叠纸飞机一样," 老张解释道, "从右边开始一层层折叠!" ✈️

(2) 一元左折叠 (向左展开)

template<typename... Args>
void print_left(Args... args) {
    // 从左向右展开: ((a1 + a2) + a3) + a4 ⬅️
    (该例子不恰当,以后会改😭<< args << std::cout)  // 从左向右展开
}

"这次是从左边开始折," 小王恍然大悟, "就像叠信封一样!" ✉️

(3) 二元右折叠 (带初始值)

template<typename... Args>
auto sum_right(Args... args) {
    return (args + ... + 100);  // 右边带初始值: a1 + (a2 + (a3 + 100)) 🎯
}

"哦!" 小王眼睛一亮, "这就像做蛋糕,最后要放个樱桃在顶上!" 🍒

(4) 二元左折叠 (带初始值)

template<typename... Args>
auto sum_left(Args... args) {
    return (100 + ... + args);  // 左边带初始值: ((100 + a1) + a2) + a3 🎯
}

"对啦!" 老张点点头, "就像搭积木,要先放个底座!" 🏗️

"哦!明白了!" 小王眼睛一亮,"就像叠纸一样,可以从左边开始叠,也可以从右边开始叠!" 🎯

"没错!" 老张点点头,"而且带初始值的版本更安全,就像叠纸前先打好底一样!" 🏗️

实战修炼

"诶,小王," 老张眨眨眼睛 😉,"来个实战练习怎么样?"

"什么练习?" 小王立刻来了精神 🌟

"写个函数,能一次性打印多个参数,要用折叠表达式哦!" 老张露出狡黠的笑容 🦊

小王思考片刻,眼睛一亮 💡:

template<typename... Args>
void print(Args... args) {
    (std::cout << ... << args) << "\n";  // 折叠魔法 ✨
}

"哇!这也太简单了吧!" 小王惊喜地喊道 🎉

"对啊!" 老张点点头,"用起来更简单:"

print("Hello", 42, 3.14, "World");  // 一行搞定 🚀
// 输出: Hello423.14World

"这比写一堆重载函数爽多了!" 小王击掌欢呼 🙌

"没错," 老张笑着说,"这就是现代C++的魅力!" ✨

注意事项小贴士

"诶,小王,折叠表达式虽好,但也有个坑要注意!" 老张突然严肃起来 🤨

"什么坑啊?" 小王紧张地问 😰

"空参数包的问题!" 老张竖起食指 ☝️

template<typename... Args>
bool all(Args... args) {
    return (... && args);    // 安全 ✅
    // return (... + args);  // 危险 ❌
}

"哦!原来只有 &&、|| 和逗号运算符才能安全处理空参数包!" 小王恍然大悟 💡

"对头!" 老张点点头,"就像自动门虽然方便,但停止时还得靠人工开关一样!" 🚪

折叠表达式的语法细节

"小王,来看看折叠表达式的四种基本形式!" 老张拿起马克笔,在白板上画起来 ✍️

// 第一种: 一元右折叠 - 像叠纸飞机一样从右往左折 ✈️
(pack op ...)         
// 例如: (args + ...) 会展开成 a1 + (a2 + (a3 + a4))

"哦!这就像从右边开始叠纸飞机!" 小王恍然大悟 💡

// 第二种: 一元左折叠 - 像叠信封一样从左往右折 ✉️
(... op pack)         
// 例如: (... + args) 会展开成 ((a1 + a2) + a3) + a4

"对,再看看带初始值的版本:" 老张继续写道:

// 第三种: 二元右折叠 - 最后再加个樱桃 🍒
(pack op ... op init) 
// 例如: (args + ... + 100) 变成 a1 + (a2 + (a3 + 100))

// 第四种: 二元左折叠 - 先放个底座再开始 🏗️
(init op ... op pack) 
// 例如: (100 + ... + args) 变成 ((100 + a1) + a2) + a3

"这里的 op 可以用很多运算符哦!" 老张解释道,"我们把它们分类一下:" 📝

// 1️⃣ 算术运算符 - 做数学计算用
+, -, *, /, %

// 2️⃣ 位运算符 - 处理二进制位
^, &, |, <<, >>

// 3️⃣ 赋值运算符 - 存储值用
=, +=, -=, *=, /=, %=, ^=, &=, |=, <<=, >>=

// 4️⃣ 比较运算符 - 判断大小关系
==, !=, <, >, <=, >=

// 5️⃣ 逻辑运算符 - 处理真假值
&&, ||

// 6️⃣ 其他特殊运算符
,(逗号), .*, ->*

"哇!原来可以用这么多运算符!" 小王惊叹道 🤩

"是的,不同的运算符可以实现不同的功能。" 老张笑着说,"就像厨师的各种刀工一样,要用对工具!" 🔪

实用示例大放送

"来看几个实际应用吧!" 老张兴致勃勃地说。

(1) 打印神器 🖨️

template<typename... Args>
void printer(Args&&... args) {
    (std::cout << ... << args) << '\n';  // 一元左折叠
}

"看这个!" 老张指着代码说,"用一元左折叠实现打印,就像串糖葫芦一样,一个个打印出来!" 🍡

使用示例:

printer("你好", 42, "世界", 3.14);  // 输出: 你好42世界3.14

(2) 类型极限探索者 📊

template<typename... Ts>
void print_limits() {
    ((std::cout << +std::numeric_limits<Ts>::max() << ' '), ...) << '\n';
}

"这个更有意思," 老张解释道,"它能打印出不同类型的最大值。逗号运算符配合折叠表达式,就像魔术师变戏法一样!" 🎩

使用示例:

print_limits<char, int, long>();  // 输出: 127 2147483647 9223372036854775807

(3) Vector 快速填充器 🚀

template<typename T, typename... Args>
void push_back_vec(std::vector<T>& v, Args&&... args) {
    // 先检查类型是否匹配,就像检查钥匙能否开锁 🔐
    static_assert((std::is_constructible_v<T, Args&&> && ...)); 
    
    // 然后一个个放入vector,像往背包里装东西 🎒
    (v.push_back(std::forward<Args>(args)), ...);
}

"这个厉害了!" 小王眼前一亮,"不仅能批量添加元素,还能在编译期检查类型!"

使用示例:

std::vector<int> nums;
push_back_vec(nums, 1, 2, 3, 4, 5);  // 一次性添加多个数字 ✨

"对啊," 老张笑着说,"现代C++就是这么优雅,既安全又高效!" 🎯

"这...这简直是魔法!" 小王目瞪口呆 😱

知识点总结

"诶,老张," 小王摸着下巴思考道,"今天学到的这个折叠表达式,能帮我总结一下它的精髓吗?" 🤔

"当然可以!" 老张放下咖啡杯,"我们来对比一下新旧方案:"

传统写法的痛点 😫:

  • 需要写多个重载函数 📚
  • 递归实现复杂且难维护 🔄
  • 编译生成大量函数调用 🐌
  • 运行时性能有额外开销 ⚠️

折叠表达式的优势 🌟:

  • 一个模板搞定所有情况 ✨
  • 代码简洁优雅,易于理解 📝
  • 编译期展开,无递归开销 🚀
  • 运行时性能更优,直接内联 💪

"哦!原来如此!" 小王恍然大悟,"感觉这就像是把复杂的积木搭建,变成了优雅的折纸艺术!" 🎨

"没错!" 老张笑着说,"记住一点:现代C++的核心思想就是让复杂的事情变得简单,让危险的操作变得安全。折叠表达式就是最好的例子!" 🎯

"太棒了!这下我可以告别模板地狱了!" 小王开心地说。

"学习新特性,就要敢于拥抱变化。" 老张拍拍小王的肩膀,"让代码既简洁又高效,这才是现代C++的魅力所在!" ✨

责任编辑:赵宁宁 来源: everystep
相关推荐

2024-04-23 08:26:56

C++折叠表达式编程

2024-12-19 11:30:00

C++17CTAD代码

2024-12-27 12:00:00

C++17枚举

2013-04-10 10:58:19

LambdaC#

2021-07-16 08:26:18

折叠表达式参数

2009-08-31 17:11:37

Lambda表达式

2020-06-04 09:18:52

CTOif-else代码

2021-03-02 07:33:13

开发C#字符

2024-12-13 15:50:00

C++编程代码

2024-12-18 06:00:00

C++17C++

2019-04-16 13:30:05

表达式求值数据结构算法

2024-03-25 13:46:12

C#Lambda编程

2009-08-27 09:44:59

C# Lambda表达

2009-08-07 15:41:39

C#正规表达式

2020-06-15 08:12:51

try catch代码处理器

2009-10-12 10:11:08

Lambda表达式编写

2009-08-17 13:56:28

C#正则表达式入门

2009-08-26 16:17:23

C# Lambda表达

2009-08-27 09:57:50

C# Lambda表达

2009-08-07 15:16:10

C#正则表达式
点赞
收藏

51CTO技术栈公众号