当前位置: 首页 > news >正文

CppCon 2014 学习:The New Old Thing

这个代码展示了 C++ 的高级特性 —— 泛型 lambda 表达式 + 高阶函数(函数返回函数)+ 参数包(variadic templates) 的组合。其目的是模拟类似函数式编程中的 list/map 操作
我们逐步来理解这段代码:

第一步:定义 list

auto list = [](auto ...v) {return [=](auto access) { return access(v...); };
};
  • list(1, 2, 3) 实际上不是一个容器,而是返回了一个 lambda,它保存了参数包 v...
  • 这个 lambda 接收一个函数 access,然后调用它并把参数 v... 传进去。
    所以你可以认为:
auto l = list(1, 2, 3);  // l 是一个函数
l(access);               // access 将会被调用为 access(1, 2, 3)

它是一个**“函数包装器”**:把值 1, 2, 3 存起来,等着有人传一个函数进来处理它们。

第二步:定义 map

auto map = [](auto func) { return [=](auto ...z) {return list(func(z)...);};
};

这是一个高阶函数(返回一个函数):

  • 它接收一个函数 func
  • 返回一个函数 [](auto ...z),对这些参数 z... 应用 func(z)...
  • 然后把这些新值打包成 list(...)
    简化理解:
map(f)(1, 2, 3)list(f(1), f(2), f(3));

第三步:定义 print

auto print = [](auto v){std::cout << v; return v;
};

简单打印并返回值。

第四步:main 分析

int main(){list(1, 2, 3, 4)(map(print));
}

等价于:

auto lst = list(1, 2, 3, 4);    // lst 是个函数,接受一个函数 access
lst(map(print));               // map(print) 返回一个函数,接受参数列表// 最终执行 print(1), print(2), print(3), print(4)

所以它会输出:

1234

每个数字都是被 print() 打印的。

总结一句话:

这段代码通过 lambda 和参数包,实现了一个 函数式编程风格的 list/map/print 组合,体现了现代 C++ 的表达能力和高阶函数应用:

list(...)         → 保存参数并等待函数处理
map(func)         → 应用函数 func 到一组参数
list(...)(map(f)) → 等价于 map(f)(args)f(args...)list(...)

“Fun With Lambdas” 的内容,展示了一个关于 lambda 表达式参数展开(pack expansion)在不同编译器中行为不一致 的有趣例子。让我们一步步拆解,并深入理解问题的本质

问题描述总结

Sumant 编写了一个使用参数包展开(variadic template + lambda)的 C++ 程序,在不同编译器中输出结果不一致:

编译器输出
Clang1234
GCC4321
MSVC4321(但 ARM target 下为 1234

代码核心片段

关键行如下:

return list(print(z1), print(z2), print(z3), print(z4));

这可能是由以下模板代码生成的:

template<typename... Args>
auto call(Args... args) {return list(print(args)...); // 参数包展开
}
#include <iostream>
auto list = [](auto ...v) {return [=](auto access) { return access(v...); };
};
auto map = [](auto func) { return [=](auto ...z) {return list(func(z)...);};
};
auto print = [](auto v){std::cout << v; return v;
};
int main(){list(1, 2, 3, 4)(map(print));
}

编译器输出不同的原因

这不是编译器的“bug”,而是因为C++ 标准没有规定函数参数的求值顺序

C++ 标准说明

C 标准(ISO/IEC 9899 - 3.3.2.2/p9):

“函数调用中,函数名、各个参数和参数内部子表达式的求值顺序是未指定的(unspecified),但函数调用前存在一个序列点(sequence point)”。

C++ 标准(N3936 – 5.2.2/p8):

“函数参数和函数名的求值是**未排序(unsequenced)**的,它们之间的副作用顺序也不固定”。

关键点

list(print(z1), print(z2), print(z3), print(z4));

这里的 print 函数有副作用(打印输出),但由于参数的求值顺序未定义,所以:

  • Clang 按照从左到右求值 → 1234
  • GCC / MSVC 可能从右到左求值 → 4321

正确写法(有顺序保证)

如果你希望保证顺序执行(从左到右),可以使用:

(int[]){ (print(args), 0)... }; // 使用逗号表达式包裹

或者利用 fold 表达式(C++17):

(auto{} , ..., print(args)); // 从左到右保证顺序

总结

问题解答
What is the correct output?Undefined / unspecified — any of the orders are valid per standard
What is the bug?Assuming a fixed evaluation order of function arguments during pack expansion, which the standard does not guarantee
Fix?Use sequencing constructs like fold expressions or initializer lists to enforce evaluation order

你提到的内容揭示了 C++ 中一个更广泛的问题(A Wider Problem) —— 子表达式的求值顺序未定义(unspecified order of evaluation),这可能导致令人惊讶的行为,甚至隐藏 bug。我们来详细分解理解这个问题。

问题一:赋值中未定义的求值顺序

std::vector<int> v;
int i = 0;
v[i] = ++i;

这是干什么的?

  1. i 初始化为 0。
  2. v[i] 用于访问向量的元素(i == 0)。
  3. ++ii 增加为 1。
    问题:i++i 的求值顺序未定义。
  • 如果 i 被先用作下标,再执行 ++i:相当于 v[0] = 1;
  • 如果 ++i 被先执行,再 v[i]:相当于 v[1] = 1;
    结果依赖于编译器,标准没有定义顺序 → 易错!

问题二:成员函数调用中的顺序未定义

f1()->mf(f2());

这是干什么的?

  1. f1() 返回一个对象指针。
  2. f2() 是参数传递给 mf(...)
    问题:f1()f2() 的调用顺序是未定义的
  • f1()f2() 谁先被调用?标准没有规定。
    这意味着:
  • 如果 f2() 有副作用(例如修改全局状态、日志、计时等),你可能得到不一致行为。
  • 如果 f1() 的返回值依赖于某个状态,而该状态被 f2() 改变,也可能出问题。

背后的标准说明

根据 C++ 标准:

  • 表达式内部子表达式之间的求值顺序默认是未定义的(例如函数参数、运算符的左右操作数等)。
  • 除非使用 sequence point(序列点)sequenced-before(有序) 规则明确顺序,例如:
    • &&, ||, ,(逗号运算符)等控制求值顺序。

是否有解决方案?

是的,但有取舍:

建议:为避免未定义行为或不一致行为,开发者应主动控制顺序。

可行的方式:

方式示例
拆成多个语句int index = i; int val = ++i; v[index] = val;
使用中间变量auto obj = f1(); auto arg = f2(); obj->mf(arg);
使用标准序列控制操作符(..., print(args)); (fold expression)
C++23 之后可能引入更多顺序保证机制(尚未普遍实现)N/A

为什么标准不直接“修复”它?

虽然可以“强制左到右求值”,但:

性能影响

  • 一些硬件架构或优化策略依赖于自由的求值顺序,为了最大化性能。

行为改变

  • 强制固定顺序后,会改变历史上合法但不一致代码的行为,破坏向后兼容性。

总结

问题描述
子表达式求值顺序未定义比如:v[i] = ++i;f1()->mf(f2());
为什么危险?编译器可能给出不同结果,易导致难以发现的 bug
有解吗?有,但需要手动拆解、使用中间变量或 fold 表达式
C++ 标准为何不修?为了性能、灵活性和兼容性,标准保留求值自由度
http://www.xdnf.cn/news/793729.html

相关文章:

  • invalid domain [10.230.90.11:2025] was specified for this cookie异常原因分析
  • 小黑一步步探索大模型应用:langchain中AgentExecutor的call方法初探demo(智能体调用)
  • OD 算法题 B卷【通过软盘拷贝文件】
  • C++结构体初始化方式区别
  • Windows下将Nginx设置注册安装为服务方法!
  • 爱普生有源晶振SG2520CBN在通信基站中的应用
  • UVa12298 Super Joker II
  • AI一周事件(2025年5月27日-6月2日)
  • JavaScript 递归构建树形结构详解
  • linux学习第19、20天(父子进程)
  • 选择正确的电平转换解决方案
  • HertzBeat的告警规则如何配置?
  • Flowith,有一种Agent叫无限
  • MyBatis 深度解析:高效 Java 持久层框架实践指南(基于 3.5.10)
  • 黑马程序员TypeScript课程笔记—class篇
  • windows环境下Ubuntu系统怎么重置root密码
  • 鸿蒙5.0项目开发——横竖屏切换开发
  • 深入解析 Java 中的 synchronized:从使用到底层原理的全面详解
  • C++中锁和原子操作的区别及取舍
  • 楼宇自控系统联动暖通空调:解密建筑环境舒适度提升路径
  • 域自适应 (Domain Adaptation,DA)基础
  • JS对数据类型的检测
  • TitanIDE智算版:一键开启云端算法开发环境
  • Servlet 生命周期
  • 高性能MCU的MPU与Cache优化详解
  • 线性动态规划
  • 张雪峰为9岁女儿申请40个左右商标!
  • 超声波粒度仪市场报告:行业现状、竞争格局与未来趋势分析
  • 原子操作与非原子操作
  • RTOS,其高级使用