C11标准(ISO/IEC 9899:2011)引入了表达式中副作用排序的新定义(参见相关问题)。序列点概念得到了前序和后序关系的补充,这些关系现在是所有定义的基础。
第6.5节“表达”,第2点说:
如果标量对象上的副作用相对于同一标量对象上的不同副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义。如果表达式的子表达式有多个允许的顺序,则如果这种未排序的副作用发生在任何顺序中,则行为未定义。
稍后,第6.5.16节“赋值运算符”第3点指出:
更新左操作数的存储值的副作用是在左和右操作数的值计算之后进行的。操作数的求值是不排序的。
引用的第一段(6.5/2)有两个例子支持(与C99标准中的相同):
a[i++] = i; //! undefined
a[i] = i; // allowed
这可以很容易地用定义来解释:
因此,i(LHS)的副作用与i
(RHS)没有测序,这给出了未定义的行为。
i = ++i + 1; //! undefined
i = i + 1; // allowed
但是,在两种给定情况下,此代码似乎都会导致定义的行为,如下所示:
因此,i1
的执行应先于更新i
产生的副作用,这意味着相对于同一标量对象上的不同副作用或使用同一标量的值进行的值计算,未排序的标量对象上没有副作用。
用C99标准提供的术语和定义很容易解释这些例子(见相关问题)。但是为什么根据 C11 的术语没有定义 i = i 1
?
[更新]
我在这里更改我的答案,尽管在 C11 中,但这在 C11 中没有很好的定义。这里的关键是 i 的结果不是左值,因此在计算 i 后不需要左值到右值的转换,因此我们不能保证之后会读取
i
的结果。
这与 C 不同,因此我最初链接到的缺陷报告取决于以下关键事实:
[…]左值表达式i,然后对结果进行左值到右值的转换。保证在计算加法运算之前对递增副作用进行排序[…]
我们可以通过转到 C11 草案标准第 6.5.3.1
节前缀递增和递减运算符来看到这一点,其中说:
[…]表达式E等价于(E=1)。[…]
然后是第6.5.16
赋值运算符,它说(强调我的未来):
赋值运算符将值存储在由左操作数指定的对象中。赋值表达式具有赋值后左操作数的值,<sup>111</sup>,但不是左值。[...]
脚注111
表示:
允许实现读取对象以确定值,但不需要读取,即使对象具有volatile限定类型。
不需要读取对象来确定它的值,即使它是易失性的。
原始答案
据我所知,这实际上是很好的定义,并且此示例已从使用类似语言的C草案标准中删除。我们可以在 637 中看到这一点。排序规则和示例不一致,其中说:
以下表达式仍然作为未定义行为的示例列出:
i = ++i + 1;
然而,新的排序规则似乎使这一表达式得到了很好的定义:
解决方案是取消前缀示例,而使用后缀示例,这显然是未定义的:
将 1.9 [介绍执行] 第 16 段中的示例修改如下:
i=ii 1;//行为未定义
标准规定了作业(6.5.16),因为你引用的是正确的
更新左操作数的存储值的副作用在左操作数和右操作数的值计算之后排序。
(增量运算符没有什么不同,只是变相的赋值)
这意味着有两个值计算(左和右),赋值的副作用在这些之后排序。但它只是根据价值计算排序,而不是根据这些可能产生的副作用排序。因此,最终我们会面临两个副作用(< code>=操作符和< code> 操作符),这两个副作用相互之间没有顺序关系。
但是为什么根据 C11 的术语没有定义 i = i 1
?
C11表示左i
的副作用是有序的,但不是左和右i
的值计算(评估)。
很明显,对LHS的副作用将发生在对LHS和RHS的表达式进行评估之后。
为了解释这一点,一个更好的例子可能是
int i = 1;
i = i++ + 3;
(首先让我们假设这个例子不会调用UB)。现在i
的最终值可以是4
或2
。
案例1。
左i
被获取,然后它被递增,3
被添加到其中,最后4
被分配给i
。
案例2。
左i
被获取,然后3
被添加到其中,然后4
被分配给i
,最后i
被递增。在这种情况下,i
的最终值是2
。
虽然左边i
的副作用被排序,但存储到i
的最终值没有定义,即它不一定是由赋值决定的,因此i
的副作用是无序的。