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++ 程序,在不同编译器中输出结果不一致:
编译器 | 输出 |
---|---|
Clang | 1234 |
GCC | 4321 |
MSVC | 4321 (但 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;
这是干什么的?
i
初始化为 0。v[i]
用于访问向量的元素(i == 0
)。++i
把i
增加为 1。
问题:i
和++i
的求值顺序未定义。
- 如果
i
被先用作下标,再执行++i
:相当于v[0] = 1;
- 如果
++i
被先执行,再v[i]
:相当于v[1] = 1;
结果依赖于编译器,标准没有定义顺序 → 易错!
问题二:成员函数调用中的顺序未定义
f1()->mf(f2());
这是干什么的?
f1()
返回一个对象指针。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++ 标准为何不修? | 为了性能、灵活性和兼容性,标准保留求值自由度 |