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

Nav*_*vin 34 c c++ operator-precedence sequence-points c++03

我已经查看了一系列有关序列点的问题,并且无法x*f(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;
}
Run Code Online (Sandbox Code Playgroud)

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

我想知道这是保证行为还是未指明.

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

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

另外:如果我改变所有要使用的函数int *x而不是对int &x它们调用的地方进行相应的更改,我会得到C具有相同问题的代码.C的答案有何不同?

Mak*_*jov 22

就评估顺序而言,更容易将其x*f(x)视为:

operator*(x, f(x));
Run Code Online (Sandbox Code Playgroud)

因此,对乘法应该如何工作没有数学上的先入之见.

正如@ dan04所指出的那样,标准说:

第1.9.15节:"除非另有说明,否则对个体操作员的操作数和个别表达式的子表达式的评估是不合理的."

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

在你的例子中,从概念上讲,你可以确定至少有一个参数是7,但是你不能确定它们都是.对我来说,这足以将此行为标记为未定义; 但是,@ user2079303答案解释了为什么技术上并非如此.

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

  • @Bathsheba:关联性只是关于运算符如何关联:它与您确定参数实际是什么的顺序无关. (14认同)
  • @Bathsheba:从左到右的关联性意味着`a*b*c`被定义为等同于`(a*b)*c`而不是`a*(b*c)`,但编译器是仍然可以在`a`或`b`之前自由地评估子表达式`c`. (10认同)
  • 你确定吗?乘法的*关联性*是从左到右.我不认为允许编译器以任何顺序评估参数.但是,我认为,它可能在乘法之前评估f(x).因此,结果未指定. (2认同)
  • 有一个C++标准的*草案*可在http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf上找到.第1.9.15节指出"除非另有说明,否则对个体操作员的操作数和个别表达式的子表达式的评估是不合理的." (2认同)
  • @Synxis:你错过了一些东西:`f`的调用不能与另一个术语的'x`重叠,它是不确定的顺序(意思是之前或之后,未指定).因此,我们没有获得UB. (2认同)
  • 内联不会改变程序序列的定义.在内联函数时,确保它不会破坏格式良好的代码是编译器的工作.优化可能会改变*未指定的*效果,但如果之前没有未定义的行为,则编译器不能引入未定义的行为.如果您还没有:),请参阅我的答案以获取详细的文章 (2认同)

eer*_*ika 12

参数的评估顺序不是由标准指定的,因此不能保证您看到的行为.

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

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

除非另有说明,否则单个运算符的操作数和个别表达式的子表达式的评估顺序以及副作用的发生顺序是未指定的...


还讨论了这是否是未定义的行为或仅仅是未指定的顺序.该段的其余部分对此提出了一些启示(或怀疑).

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

...在前一个和下一个序列点之间,标量对象的表达式评估最多只能修改一次存储值.此外,只能访问先前值以确定要存储的值.对于完整表达式的子表达式的每个允许排序,应满足本段的要求; 否则行为未定义.

x确实被修改了,f并且它的值在f被调用的同一表达式中被读作操作数.并且未指定是否x读取修改或未修改的值.这可能会尖叫Undefined Behavior!给你,但是抱着你的马,因为标准还规定:

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

...当调用函数时(无论函数是否为内联函数),在评估函数体中任何表达式或语句之前发生的所有函数参数(如果有)之后,都会有一个序列点.复制返回值之后和执行函数外的任何表达式之前还有一个序列点11) ...

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

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

总之,订单未指定,但没有未定义的行为.这是一个错误,但结果在某种程度上是可以预测的.即使措辞发生变化,后续标准中的行为也是相同的.我不会深入研究那些因为它已经在其他好的答案中得到了很好的解释.


既然你在C中询问类似的情况

C89(草案)3.3/3:

除非语法27指示或稍后指定(对于函数调用运算符(),&&,||,?:和逗号运算符),子表达式的计算顺序和副作用的发生顺序为:都未指明.

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

C89(草案)3.3/2:

在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的计算来修改一次.此外,只能访问先前值以确定要存储的值.26

以下是定义的序列点:

C89(草案)A.2

以下是2.1.2.3中描述的序列点

  • 在评估参数之后调用函数(3.3.2.2).

  • ...

  • ...表达式在return语句中(3.6.6.4).

结论与C++中的结论相同.


Use*_*ess 7

关于我没有看到的东西的快速说明由其他答案明确涵盖:

如果计算的顺序为x*f(x)是,如果保证f修改x,并且是用于本不同f(x)*x.

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

operator*(x, f(x));
Run Code Online (Sandbox Code Playgroud)

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

auto lhs = x;        // or auto rhs = f(x);
auto rhs = f(x);     // or auto lhs = x;
    return lhs * rhs
Run Code Online (Sandbox Code Playgroud)

所以,当你问

我想知道这是保证行为还是未指明.

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

所以,它既不保证也不完全不指定.


哦,并且:

我看了一堆关于序列点的问题,并且无法弄清楚评估的顺序是否......

序列点是用于C语言标准的处理,但不是在C++标准中.

  • 短语"序列点"可能已从C++ 11及更高版本的标准中删除,但它*曾用于以前的C++标准([参考](https://isocpp.org/wiki/faq/misc-technical) -issues#sequence-points)). (4认同)

M.M*_*M.M 5

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

  • A 在之前 进行测序B:AB开始评估之前,必须对所有副作用进行评估
  • A并且B 不确定地测序:以下两种情况之一是真的:A在之前测序B,或B在之前测序A.它是不确定的这两个案件成立.
  • AB 未测序:有之间没有定义排序关系AB.

重要的是要注意这些是成对的关系.我们不能说" x没有统治".我们只能说两个操作相互之间没有排序.

同样重要的是这些关系是可传递的 ; 后两种关系是对称的.


未指定的是技术术语,表示标准指定了一组可能的结果.这与未定义的行为不同,这意味着标准根本不包括行为.请参阅此处以进一步阅读


转到代码x * f(x).这是相同的f(x) * x,因为如上所述,x并且在两种情况下相对于彼此f(x)都是无序的.

现在我们已经到了几个人似乎正在崩溃的地步.计算表达式f(x)是未测序相对于x.但是,它并没有遵循的函数体中的任何声明f,也未测序相对于x.事实上,任何函数调用都存在顺序关系,这些关系不容忽视.

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

当调用函数时(无论函数是否为内联函数),与任何参数表达式相关联的每个值计算和副作用,或者使用指定被调用函数的后缀表达式,都会执行每个表达式或语句之前对其进行排序.叫功能.[注意:与不同参数表达式相关的值计算和副作用未被排序.-end note] 调用函数(包括其他函数调用)中的每个评估在执行被调用函数体之前或之后没有特别排序,对于被调用函数的执行是不确定的.

脚注:

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

粗体文字明确指出两种表达方式:

  • :x = x + 1;里面f(x)
  • B:评估x表达式中的第一个x * f(x)

他们的关系是:不确定地排序.

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

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

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

结果是根据先前是顺序排序还是相反排序而未指定.因此,只有两种可能的结果,并且.xx = x + 14249


如果任何人有关于疑虑xf(x),下列文本适用:

当调用函数时(无论函数是否为内联函数),与任何参数表达式相关联的每个值计算和副作用,或者使用指定被调用函数的后缀表达式,都会执行每个表达式或语句之前对其进行排序.叫功能.

所以之前对它的评估x有序的 x = x + 1.这是一个爆炸的例子,属于上面粗体引用中"特定顺序 " 的情况.


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