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

Is value of x*f(x) unspecified if f modifies x?

本文关键字:未指定 修改 如果      更新时间:2023-10-16

我看了一堆关于序列点的问题,并且没有能够弄清楚如果f修改x, x*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返回后获取x的值),而行B计算6*7(在fx返回之前获取x的值)。

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

也:如果我将所有函数更改为使用int *x而不是int &x,并对它们被调用的地方进行相应的更改,我将获得具有相同问题的C代码。C的答案有什么不同吗?

在求值序列方面,更容易将x*f(x)想象为:

operator*(x, f(x));

这样就不会对乘法的运算方式有数学上的先入之见。

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

第1.9.15节:"除特别说明外,单个操作符的操作数和单个表达式的子表达式的求值都是非排序的。"

这意味着编译器可以自由地以任何顺序计算这些参数,序列点是operator*调用。唯一可以保证的是,在调用operator*之前,两个参数都必须求值。

在您的示例中,从概念上讲,您可以确定至少有一个参数为7,但您不能确定两个参数都为7。对我来说,这足以将这种行为定义为未定义;然而,@user2079303的答案很好地解释了为什么技术上不是这样的。

无论行为是未定义的还是不确定的,都不能在行为良好的程序中使用这样的表达式。

参数的求值顺序是而不是标准指定的,因此您看到的行为不能得到保证。

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

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

除特别说明外,单个操作符和单个表达式的子表达式的操作数的求值顺序以及副作用发生的顺序均未指定…


还讨论了这是未定义的行为还是只是未指定的顺序。

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

…在前一个序列点和下一个序列点之间,标量对象的存储值最多只能通过表达式的求值修改一次。此外,只有在确定要存储的值时才能访问先验值。对于一个完整表达式的子表达式的每一个允许的顺序,都应满足本款的要求;否则行为未定义。

x确实在f中被修改了,它的值在调用f的同一个表达式中作为操作数被读取。并且没有指定x是否读取修改或未修改的值。这可能会尖叫未定义行为!对你来说,但别着急,因为标准还规定:

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

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

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

如果先求x,则在求f(x)的参数后存在一个序列点。再次,关于UB的规则不适用。在本例中,x操作数将具有未修改的值。

总的来说,顺序是未指定的,但是没有未定义的行为。这是一个漏洞,但结果在某种程度上是可以预测的。在后来的标准中,行为是一样的,尽管措辞有所改变。我不会深入探讨这些问题,因为其他好的答案已经很好地涵盖了这些问题。

既然你问C中的类似情况

C89 (draft) 3.3/3:

除非语法27指示或稍后另有指定(对于函数调用操作符(),&&, ||, ?:和逗号操作符),子表达式的求值顺序和副作用发生的顺序都未指定。

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

C89 (draft) 3.3/2:

在前一个序列点和下一个序列点之间,对象的存储值最多只能被表达式的求值修改一次。而且,只有在确定要存储的值时才能访问先验值26

这里是定义的序列点:

C89 (draft) A.2

以下是2.1.2.3

中描述的序列点
  • 在参数被求值后调用函数(3.3.2.2)。

  • ……

  • …return语句中的表达式(3.6.6.4)。

关于其他答案中没有明确提到的一些事情:

如果f修改了x, 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

所以当你问

我想知道这是保证行为还是未指定的。

标准没有指定编译器必须选择这两种行为中的哪一种,但是确实指定了这两种行为是唯一有效的。

所以,它既不是保证的,也不是完全未指定的。


哦,:

我看了一堆关于序列点的问题,还没能弄清楚如果评估的顺序…

序列点在C语言标准中使用,但在c++标准中不使用。

在表达式x * y中,xy未排序的。这是三种可能的排序关系之一,它们是:

  • A 测序-在 B之前:A必须在B开始评估之前进行评估,所有副作用都完成
  • AB indeterminated -sequenced: AB之前排序,或者BA之前排序。未指明哪一种情况成立。
  • AB unsequenced: AB之间没有确定的序列关系。
重要的是要注意这些是关系。我们不能说"x是未测序的"。我们只能说两个操作相对于彼此是无序的。

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


unspecified是一个技术术语,意思是标准规定了一组可能的结果。这与未定义行为不同,意味着标准根本不涵盖该行为。


转到代码x * f(x)。这与f(x) * x相同,因为如上所述,在这两种情况下,xf(x)都是未排序的

现在我们到了有几个人似乎陷入困境的地步。计算表达式f(x)相对于x 是未测序的。但是,不是表示f函数体中的任何语句相对于x也是无序的。实际上,在任何函数调用周围都存在排序关系,这些关系不能被忽略。

下面是c++ 14的文本:

调用函数时(无论函数是否内联),与任何参数表达式或指定被调用函数的后缀表达式相关的每个值计算和副作用都在执行被调用函数体中的每个表达式或语句之前排序。[注:与不同参数表达式相关的值计算和副作用是不排序的。]-结束提示]调用函数(包括其他函数调用)中的每个求值在之前或在被调用函数体执行之后没有明确排序,则不确定地与排序

与脚注:

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

加粗的文本清楚地表明,对于这两个表达式:

  • : x = x + 1;内部f(x)
  • B:计算表达式x * f(x)
  • 中的第一个x

它们的关系是:不确定排序

关于未定义行为和排序的文本为:

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

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

结果是未指定根据x是否在x = x + 1之前排序或相反。所以只有两种可能的结果,4249


如果有人对f(x)中的x有疑问,下面的文本适用:

调用函数时(无论该函数是否内联),与任何实参表达式或指定被调用函数的后缀表达式相关的每个值计算和副作用都在执行被调用函数体中的每个表达式或语句之前排序。

所以x的求值在 x = x + 1之前排序。这是一个计算的例子,在上面的黑体引号中属于"特别排序在之前"的情况。


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

你需要区分:

a)操作符优先级和结合性,它控制子表达式的值由其操作符组合的顺序。

b)子表达式求值的顺序。例如,在表达式f(x)/g(x)中,编译器可以先计算g(x),然后再计算f(x)。当然,结果值必须通过按正确的顺序除以各自的子值来计算。

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

作为一个非常粗略的近似,你可以说,在单个表达式中,求值的顺序(不是结合律等)或多或少是未指定的。如果需要特定的求值顺序,可以将表达式分解为一系列语句,如下所示:

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++操作符的操作数的求值顺序为未指明的。编译器可以按任何顺序计算操作数,并且可以当同一表达式再次求值时,选择另一个顺序

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

求值顺序