C++加法表达式中自增/自减运算符的求值顺序相关疑问
1. (*itMid + *--itMid)/2.0 的求值顺序无标准保证
这段代码属于C++标准定义的未定义行为,你提到的两种求值顺序都可能出现,当前在MSVC、GCC下测试到的一致结果只是编译器的偶然实现选择,既不具备跨编译器可移植性,也不保证未来编译器版本不会调整行为。
C++17版本之前,加法运算符+的左右两个操作数的求值顺序完全未做规定,既不保证左操作数先求值,也不保证右操作数先求值,表达式中副作用(此处指--itMid对迭代器的修改)的发生时机也没有顺序约束。你写的表达式中,左操作数是*itMid,右操作数是*--itMid,编译器完全可以先执行右操作数的自减逻辑,再读取左操作数中迭代器指向的值,此时两次解引用拿到的是同一个位置的元素,计算出的中位数结果完全错误。
原问题中的示例代码:
std::vector<double> v{ 3,5,2,8,1,2 }; auto itMid = v.begin() + v.size() / 2; std::partial_sort(v.begin(), itMid+1, v.end()); auto med = (*itMid + *--itMid)/2.0;
你预期的执行逻辑:
auto tmp1 = *itMid; --itMid; auto tmp2 = *itMid; auto med = (tmp1 + tmp2) / 2.0;
但标准完全允许编译器按以下错误逻辑执行:
--itMid; auto tmp1 = *itMid; auto tmp2 = *itMid; auto med = (tmp1 + tmp2) / 2.0;
2. 修改后的(*--itMid + *--itMid)/2.0 存在更严重的未定义行为
这段代码的未定义风险比第一段更高,你提到的“两次自减先全部执行,再做两次解引用取同一个值”的情况,完全在标准允许的实现范围内。
修改后的示例代码:
std::vector<double> v{ 3,5,2,8,1,2 }; auto itMid = v.begin() + v.size() / 2; std::partial_sort(v.begin(), ++itMid, v.end()); auto med = (*--itMid + *--itMid)/2.0;
该表达式的两个操作数都包含对同一个变量itMid的前置自减副作用,C++标准明确规定:如果同一个标量对象的两个副作用在表达式中属于未排序关系,程序行为直接归为未定义。不要抱有“哪个自减先执行结果都一样”的侥幸——编译器不需要遵循“计算完一个操作数的全部逻辑再算下一个”的规则,完全可以先完成两次自减操作,再执行两次解引用,此时拿到的是排序后数组更靠前位置的元素,计算结果完全错误;甚至编译器可能因为检测到未定义行为直接优化掉整段逻辑,输出完全不可预期的结果。
3. 表达式内部嵌套自增/自减属于典型不良实践
在无法100%确认自增/自减的副作用和同表达式内其他值访问不存在顺序冲突的前提下,在表达式内部嵌套自增/自减是被几乎所有主流C++编码规范明确规避的写法,原因非常实际:
- 自增/自减是带副作用的操作,把这类操作塞进复杂表达式,本质是把多个独立执行步骤压缩到同一行,哪怕编写者自己清楚逻辑,后续维护者也很容易误读,引入隐蔽bug。
- 这类写法的正确性完全依赖编译器的偶然实现,编译器版本更新、编译选项调整(比如开启高等级优化、切换编译目标架构)都可能改变未定义行为的表现,排查这类问题的时间成本极高。
- 把带副作用的自增/自减拆成独立语句、提前把需要的值存入局部变量的写法,逻辑完全清晰,不存在任何求值顺序歧义,且编译器开启优化后生成的机器码和紧凑写法的运行效率没有任何区别。
以中位数计算为例,安全无歧义的写法如下:
std::partial_sort(v.begin(), itMid + 1, v.end()); double right_median = *itMid; --itMid; double left_median = *itMid; double med = (left_median + right_median) / 2.0;
内容的提问来源于stack exchange,提问作者mentalmushroom

