表达式的定义行为

11

C99标准在$6.5.2中规定:

在前一个和下一个序列点之间,一个对象的存储值最多只能被一个表达式的求值修改一次。此外,之前的值只能被读取来确定要存储的值

(强调是我的)

它继续指出,以下示例是有效的(起初似乎很明显)

a[i] = i;

虽然它没有明确说明ai是什么。

虽然我认为它并没有,但我想知道这个例子是否涵盖了以下情况:

int i = 0, *a = &i;
a[i] = i;

这将不会改变i的值,但是访问i的值以确定放置该值的地址。那么,给i赋一个已经存储在i中的值是否无关紧要?请说明一下。


奖励问题: a[i]++a[i] = 1呢?


当然你可以很傻 - 而 C 让你这样做。但为什么呢? - Ed Heal
保持你的代码简单易懂。下一个读这份代码的人可能就是你自己,也可能是其他人。如果你不把代码写得清晰易懂,那么你只能自己为难或者让其他人为难了。选择权在你手中。 - Ed Heal
2个回答

15

第一句话:

 

在前一个和下一个序列点之间,通过表达式的求值修改对象的存储值最多只能一次。

这句话已经很清楚了。语言不会强制对子表达式进行排序,除非它们之间有一个序列点,并且它不要求任何未指定的评估顺序,而是说两次修改一个对象会产生未定义的行为。这允许进行激进的优化,同时仍然可以编写遵循规则的代码。

接下来的句子:

 

此外,先前的值只能被读取以确定要存储的值

乍一看确实有点令人费解;为什么读取值的目的会影响表达式是否具有定义行为?

但是,它反映出如果子表达式B依赖于子表达式A的结果,则必须首先评估A,然后才能评估B。C90和C99标准没有明确说明这一点。

该句的更明显的违例,在脚注中给出了一个示例:

a[i++] = i; /* undefined behavior */
假设a是一个已声明的数组对象,i是一个已声明的整数对象(没有指针或宏技巧),每个对象不会被修改超过一次,因此不违反第一句话。但是,在左侧的i++的评估确定要修改哪个对象,在右侧i的评估确定要存储在该对象中的值 - 读取操作RHS和写入操作LHS的相对顺序未定义。同样,语言可以要求子表达式按某种未指定的顺序进行评估,但实际上它将整个行为留作未定义,以允许更积极的优化。
在你的例子中:
int i = 0, *a = &i;
a[i] = i; /* undefined behavior (I think) */

读取i的上一个值既是为了确定要存储的值,也是为了确定要将其存储到哪个对象中。由于a[i]引用了i(但仅因为i == 0),修改i的值会改变左值表达式a[i]所引用的对象。在这种情况下,存储在i中的值恰好与先前存储在那里的值相同(为0),但标准对偶然存储相同值的写入操作也没有例外。我认为这种行为是未定义的。(当然,标准中的示例并不意味着覆盖此案例;它隐含地假设a是一个声明的数组对象,与i无关。)

至于标准允许的示例:

int a[10], i = 0; /* implicit, not stated in standard */
a[i] = i;

有人可能会解释这个标准是未定义的。但我认为第二个句子只适用于由表达式修改的对象的值。 i 从未被表达式修改,因此没有冲突。 i 的值用于确定要修改的对象和要在那里存储的值,但这没问题,因为 i 的值本身永远不会更改。 i 的值不是“先前的值”,它只是值。

C11 标准有一个新的模型来评估这种表达式 - 或者说,它用不同的话来表达相同的模型。 它不是关于“序列点”,而是讨论副作用在彼此之前或之后排序,或与彼此无序。 它明确了这样一个想法:如果子表达式 B 取决于子表达式 A 的结果,则必须在评估 B 之前评估 A。

N1570 草案 的第 6.5 节中:

1 表达式是一系列运算符和操作数,指定计算值或指定对象或函数,生成副作用或执行组合。 运算符的操作数的值计算在运算符结果的值计算之前进行。

2 如果标量对象上的副作用与同一标量对象上的不同副作用或使用相同标量对象的值计算无序,则行为未定义。 如果表达式的子表达式有多个允许的排序方式,则如果出现了这样的未排序副作用,则行为未定义。

3 操作符和操作数的分组由语法指示。 除非另有规定,否则子表达式的副作用和值计算是无序的。


@hvd:评估子表达式i++(以及评估a的结果)确定要由赋值修改的对象。该结果恰好是i的先前值。 - Keith Thompson
行为未定义性早在现代优化思想之前就存在。我怀疑其未定义的主要原因有:(1)有许多情况下,意外别名的“自然平台后果”可能导致任意被破坏的值,特别是在对多字值进行“原地”操作时,并且很少有感知到将被破坏的值与“任何事情都可能发生”的区别的好处;(2)捕获此类用法的功能可能是有用的,但由于C标准没有关于陷阱可以做什么和不能做什么的规定,允许陷阱意味着允许未定义的行为。 - supercat

2

将对象的值读取以确定存储位置并不算是“确定要存储的值”。这意味着唯一争议的焦点只能是我们是否“修改”了对象i:如果我们是,那么它是未定义的;如果我们不是,则可以。

将值0存储到已经包含值0的对象中是否算作“修改存储的值”?根据“修改”的平凡英语定义来说,我会说不算;保持不变是修改的反义词。

然而,很明显这将是未定义行为:

int i = 0, *a = &i;
a[i] = 1;

毫无疑问,存储的值被读取是为了除了确定要存储的值之外的其他目的(要存储的值是一个常量),并且i的值被修改。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接