如果f修改x,x * f(x)的值是未指定的?

我查看了一系列有关序列点的问题,并且一直没有弄清楚如果f修改xx*f(x)的评估顺序是否有保证,而f(x)*x

考虑这个代码:

 #include <iostream> int fx(int &x) { x = x + 1; return x; } int f1(int &x) { return fx(x)*x; // Line A } int f2(int &x) { return x*fx(x); // Line B } int main(void) { int a = 6, b = 6; std::cout << f1(a) << " " << f2(b) << std::endl; } 

这将在g ++ 4.8.4(Ubuntu 14.04)上打印49 42

我想知道这是保证行为还是不明确的。

具体来说,在这个程序中, fx被调用两次,两次都是x=6 ,两次都返回7。 不同之处在于,A线计算7 * 7(取fx返回后fx值),而B线计算6 * 7(取fx返回前fx值)。

这是保证的行为? 如果是,标准的哪一部分规定了这一点?

另外:如果我改变所有的函数使用int *x而不是int &x并且对它们被调用的地方做出相应的改变,我得到的C代码有相同的问题。 C的答案有什么不同吗?

就评估顺序而言,将x*f(x)看作是:

 operator*(x, f(x)); 

所以对乘法应该如何工作没有math上的偏见。

正如@ dan04有用地指出的那样,标准说:

第1.9.15节:“除非另有说明,个别操作符的操作数和个别expression式的操作数的评估是不确定的。”

这意味着编译器可以以任何顺序自由地评估这些参数,序列点是operator* call。 唯一的保证就是在调用operator*之前,必须对两个参数进行评估。

在你的例子中,在概念上,你可以肯定至less有一个参数是7,但是你不能确定它们两个都会。 对我来说,这足以将此行为标记为未定义; 然而,@ user2079303答案解释了为什么它不是技术上的情况。

不pipe行为是不确定的还是不确定的,你都不能在一个行为良好的程序中使用这样的expression。

参数的评估顺序没有被标准规定,所以你看到的行为是不能保证的。

既然你提到顺序点,我会考虑使用该术语的c ++ 03标准,而后来的标准已经改变了措辞并放弃了这个术语。

ISO / IEC 14882:2003(E)§5/ 4:

除了注明的地方外,个别操作符的操作数和个别expression式的次expression式的评估顺序以及副作用发生的顺序是未指定的。


还有关于这是不确定的行为还是仅仅是未明确的订单的讨论。 该段的其余部分对此作了一些阐述(或怀疑)。

ISO / IEC 14882:2003(E)§5/ 4:

…在前一个序列点和下一个序列点之间,一个标量对象应该通过评估一个expression式来最多修改其存储值。 此外,只有在确定要存储的值时才能访问先前值。 对于一个完整expression式的子expression式的每个可允许的sorting,应满足本段的要求; 否则行为是不确定的。

x确实是在f修改的,它的值是在调用f的同一expression式中作为操作数读取的。 并没有规定x读取修改还是未修改的值。 这可能会尖叫未定义的行为! 给你,但抱你的马,因为标准还规定:

ISO / IEC 14882:2003(E)§1.9/ 17:

…当调用一个函数(函数是否内联)时,在执行函数体中的任何expression式或语句之前,所有函数参数(如果有的话)的求值之后都有一个序列点。 复制返回值之后,执行函数之外的任何expression式之前,还有一个顺序点。

所以,如果先评估f(x) ,那么在复制返回值之后有一个序列点。 所以关于UB的上述规则不适用,因为x的读取不在下一个和前一个序列点之间。 x操作数将具有修改后的值。

如果首先评估x ,那么在评估f(x)的参数之后有一个序列点。关于UB的规则不适用。 在这种情况下, x操作数将具有未修改的值。

总之,订单是没有指定的,但没有未定义的行为 。 这是一个错误,但结果在一定程度上是可以预测的。 后来的标准中的行为是一样的,即使措辞改变了。 我不会深究这些,因为它已经在其他好的答案中得到了很好的解决。


既然你问C中类似的情况

C89(草案)3.3 / 3:

除了语法27或稍后指定的函数调用操作符(),&&,||,?:和逗号操作符)之外,子expression式的评估顺序和副作用发生的顺序是两个都没有说明。

这里已经提到了函数调用exception。 以下是如果没有序列点,则暗示未定义行为的段落:

C89(草案)3.3 / 2:

在前一个序列点和下一个序列点之间,一个对象应该通过评估一个expression式来最多修改其存储值一次。 此外,只有在确定要存储的值时才能访问先前值。 26

这里是定义的序列点:

C89(草案)A.2

以下是2.1.2.3中描述的顺序点

  • 在对参数进行评估之后调用函数(3.3.2.2)。

  • …返回语句中的expression式(3.6.6.4)。

结论与C ++中的相同。

有关我没有看到其他答案明确涵盖的内容的简要说明:

如果x*f(x)的评估顺序在f修改x得到保证,并且对f(x)*x是不同的。

考虑一下,如马克西姆的回答

 operator*(x, f(x)); 

现在只有两种方法可以根据需要在调用之前评估两个参数:

 auto lhs = x; // or auto rhs = f(x); auto rhs = f(x); // or auto lhs = x; return lhs * rhs 

所以,当你问

我想知道这是保证行为还是不明确的。

该标准没有指定编译器必须select的那两种行为中的一种,但是它确定了这些是唯一有效的行为。

所以,既没有保证,也没有完全没有规定。


哦,还有:

我已经看了一系列有关序列点的问题,而且还没有弄清楚评估的顺序。

顺序点是在C语言标准的处理中使用的,而不是在C ++标准中。

在expression式x * yxy这两个术语是没有序列的 。 这是三种可能的sorting关系之一,它们是:

  • B开始评估之前,必须对BA 之前 A 序列进行评估,所有副作用都要完成
  • AB 不确定地sorting :以下两种情况之一是正确的: AB之前被测序,或BA之前被测序。 没有具体说明这两种情况中的哪一种。
  • AB 不确定AB之间没有定义顺序关系。

重要的是要注意这些是成对的关系。 我们不能说“ x是不确定的”。 我们只能说两个行动是相互牵扯的。

同样重要的是这些关系是传递性的 ; 后两者关系是对称的。


未指定是一个技术术语,这意味着标准规定了一系列可能的结果。 这与未定义的行为有所不同,这意味着标准并不包括行为。 看到这里进一步阅读。


移至代码x * f(x) 。 这与f(x) * x是相同的,因为如上所述,在两种情况下xf(x)是相互不相关的。

现在我们来到了好几个人似乎正在解体的地步。 评估expression式f(x) x无关。 但是, 并不是f的函数体内部的任何语句都是关于xf 。 事实上,围绕任何函数调用都有sorting关系,这些关系不能被忽略。

这里是来自C ++ 14的文本:

当调用一个函数(函数是否是内联函数)时,在任何参数expression式或者指定被调用函数的后缀expression式的每一个值计算和副作用执行每个expression式或者语句称为function。 [注:与不同参数expression式相关的值计算和副作用是不确定的。 – 注意] 调用函数的主体(包括其他函数调用)中的每个评估,在执行被调用函数的主体之前或之后,都没有特别的sorting ,就被调用的函数的执行而言是不确定的

附脚注:

换句话说,函数执行不会相互交错。

粗体文本明确指出,对于这两个expression式:

  • Ax = x + 1;f(x)
  • B :评估expression式x * f(x)的第一个x

他们的关系是: 不确定的sorting

关于未定义行为和sorting的文本是:

如果标量对象的副作用不是相对于同一个标量对象上的另一个副作用或使用相同标量对象的值进行值计算,而且它们不可能是并发的(1.10),则行为是未定义的。

在这种情况下,关系是不确定的 ,而不是无序的。 所以没有不确定的行为。

根据x是否在x = x + 1之前sorting,或者反过来,结果是没有指定的 。 所以只有两个可能的结果, 4249


如果有人对f(x)x有疑问,则适用以下文本:

当调用一个函数(函数是否是内联函数)时,在任何参数expression式或者指定被调用函数的后缀expression式的每一个值计算和副作用执行每个expression式或者语句称为function。

所以对x的评估 x = x + 1 之前sorting 。 这是在上面粗体引用的“ 以前特定sorting ”的情况下的一个例子。


脚注:C ++ 03中的行为完全相同,但术语不同。 在C ++ 03中,我们说每个函数调用的入口和出口都有一个序列点 ,因此函数内部的x的写入与函数外的x的读取至less有一个序列点是分开的。

你需要区分:

a)运算符优先级和关联性,它控制子expression式的值由其运算符组合的顺序。

b)子expression评估的顺序。 例如,在expression式f(x)/g(x) ,编译器之后可以先评估g(x)f(x) 。 但是,结果值必须按照正确的顺序除以各自的子值来计算。

c)子expression的副作用序列。 粗略地说,例如,为了优化,编译器可能决定只在expression式末尾或任何其他合适的地方将值写入受影响的variables。

作为一个非常粗略的近似,你可以说,在一个单一的expression式中,评估的顺序(而不是关联性等)或多或less是未指定的。 如果您需要特定的评估顺序,请将expression式分解为如下一系列语句:

int a = f(x); int b = g(x); return a/b;

代替

return f(x)/g(x);

有关确切的规则,请参阅http://en.cppreference.com/w/cpp/language/eval_order

几乎所有C ++操作符的操作数的评估顺序都是未指定的。 编译器可以以任何顺序评估操作数,并且可以在再次评估同一个expression式时select另一个顺序

由于评估顺序并不总是相同,因此您可能会得到意想不到的结果。

评估顺序