Gwok HiujinGwok Hiujin

The Bird of Hermes is my name, eating my wings to make me tame.

Oct 19, 2022C语言系统级编程1011 words in 5 min


『C系统级编程』深入了解表达式的 Evaluation

给定一个表达式 expression,evaluation 的过程包括:

  1. 求值(value computation)
  2. 确定副作用(Initiation of Side Effect)

求值的过程需要确定返回值 Value 和返回值类型 Value_Type,记为 <V, V_T> ;而 Side Effect 的判断标准则是程序执行环境状态的改变。

了解 evaluation 的动机是显然的。一方面,可以从值的角度帮助我们理解语法,另一方面,写过编译器中端优化的应该清楚副作用分析的重要性,这可以帮助我们理解优化。

表达式 evaluation 里一个重要的议题是求值和副作用执行顺序的问题。比如被人诟病已久的 thq C 语言中那堆毫无意义的 ++i++ 之类的题目应该得到怎样的结果,a = b++ 之类的式子又应该是先确定副作用还是先求值,等等。通过一个新定义 Sequenced Before ,可以给出这些问题的答案。

Sequenced Before

Sequenced Before 是一种 非对称可传递成对 Evaluation 之间的关系。A Sequenced Before B 的含义是 A 的 evaluation 执行在 B 的 evaluation 之前。也即:与 A 相关的求值和副作用确定 全部 在与 B 相关的求值和副作用确定之前。

C 语言定义了一系列的 sequenced point 来规范 sequenced before 这种行为,A sequenced point B 保证了 A sequenced before B。

实际上,C 语言标准中的 sequenced point 就是 C 语言程序分析中【程序点】的严格定义。

下面给出能标记出 Sequenced Point 的位置:

  • 函数名和实参 evaluation 和实际函数调用执行之间
  • 在 &&、||、逗号运算符分隔的前后两个表达式之间
  • ? : 三目运算表达式中,? 之前表达式以及之后执行的表达式之间
  • 两个 full expression 之间
    • full exp:表达式语句(分号)、if、while、do、for、return 等控制条件表达式等
    • 需要注意的是一个表达式语句(分号)的前后应该都可以标记出一个 sequenced point
  • 库函数调用之前
  • printf / scanf、fprintf / fscanf、sprint / sscanf 等一系列输入输出中按转换说明符执行完转换动作之后
  • bsearch、qsort 等比较函数调用之前和之后以及调用比较函数和对象移动之间

Sequenced point 前后的表达式执行顺序是确定的,前一定先于后,举例而言:

1
2
3
4
int i = 1;
i++;
i++;
// i must be 3

而对于夹在两个 sequenced points 之间的表达式的表达式内执行顺序,除了显式的语法规定,表达式内子表达式的 evaluation 之间顺序关系没有约定,纯看编译器的喜好。举例而言:

1
2
a = b + c;
// a = b + c 的求值必须在 b + c 求值之后,b + c 的求值必须在 b 和 c 的求值之后,但 b 和 c 的求值没有顺序要求。

这一点也就反驳了 ++i 那种破题存在的必要性:

p9BADyT.png

为了处理这种不确定现象的结果,C 语言标准规定,对于一个标量对象(Scalar Object),如果:

  • 产生两次副作用且两次副作用没有先后顺序要求,或
  • 产生的副作用和同样标量对象取值之间没有先后顺序要求

则其结果是 undefined behavior。

这给了我们一个新的启示,就是要慎用自增 / 自减运算符,因为我们对它的 90% 的使用场景都涉及了第二条规定(试想我们是否经常写出类似 a[k++] = i 或者 a = i++ 这样的语句?)。国军标禁止在运算表达式中或函数调用参数中使用 ++ 或 – 操作符。

在抽象机场景下使用自增自减运算符不会产生什么问题,因为不存在优化的需求,运算顺序是比较好确定的。但到了编译场景下(存在优化)就需要慎重考虑了。

Buy me a cup of coffee ☕.