为什么`i = ++ i + 1`未指明行为?

时间:2009-12-07 14:56:55

标签: c++ standards variable-assignment

考虑以下C ++标准ISO / IEC 14882:2003(E)引用(第5节,第4段):

  

除非另有说明,否则顺序为   评估个人的操作   运营商和个人的子表达   表达式,以及中的顺序   发生了哪些副作用,是   不确定的。 53)之间的   下一个序列指向一个标量   对象应具有其存储值   最多修改一次   表达的评价。   此外,先前的值应为   只访问以确定值   要存储。这个要求   应满足每一段   允许的订购   表达式的子表达式;   否则行为未定义。   [例如:

i = v[i++];  // the behavior is unspecified 
i = 7, i++, i++;  //  i becomes 9 

i = ++i + 1;  // the behavior is unspecified 
i = i + 1;  // the value of i is incremented 
     

-end example]

我很惊讶i = ++i + 1给出了未定义的i值。 有没有人知道在下列情况下没有给2的编译器实现?

int i = 0;
i = ++i + 1;
std::cout << i << std::endl;

事情是operator=有两个参数。第一个始终是i引用。 在这种情况下,评估顺序无关紧要。 除了C ++ Standard禁忌之外,我没有看到任何问题。

,请考虑参数顺序对评估很重要的情况。例如,++i + i显然是未定义的。请,只考虑我的情况 i = ++i + 1

为什么C ++标准禁止这样的表达?

15 个答案:

答案 0 :(得分:62)

你错误地认为operator=是一个双参数函数,其中必须在函数开始之前完全评估参数的副作用。如果是这种情况,则表达式i = ++i + 1将具有多个序列点,并且在分配开始之前将++i完全评估。但事实并非如此。在 内在赋值运算符中评估的是什么,而不是用户定义的运算符。该表达式中只有一个序列点。

++i结果在分配之前(以及在加法运算符之前)进行评估,但副作用不一定立即应用。 ++i + 1的结果始终与i + 2相同,因此这是作为赋值运算符的一部分分配给i的值。 ++i的结果始终为i + 1,因此作为增量运算符的一部分分配给i的结果。没有序列点来控制首先分配哪个值。

由于代码违反了“在前一个和下一个序列点之间,标量对象应通过表达式的求值最多修改一次其存储值”的规则,因此行为未定义。 实际上,可能首先分配i + 1i + 2,然后分配另一个值,最后程序将继续照常运行 - 否鼻子或爆炸的厕所,也没有i + 3

答案 1 :(得分:37)

这是未定义的行为,而不是(仅仅)未指定的行为,因为有两个写入i没有插入序列点。就标准规定而言,按照定义就是这种方式。

该标准允许编译器生成将写入延迟写入存储的代码 - 或者从另一个视点延迟,以重新排序实现副作用的指令 - 只要它符合序列点的要求,它就会选择任何方式。

此语句表达式的问题在于它意味着对i的两次写入没有插入序列点:

i = i++ + 1;

一次写入是i“的原始值加上一个”,另一个是该值“再加上一个”。这些写入可能以任何顺序发生,或者在标准允许的情况下完全爆炸。从理论上讲,这甚至使实现可以自由地并行执行回写,而无需检查同时访问错误。

答案 2 :(得分:15)

C / C ++定义了一个名为sequence points的概念,它指的是一个执行点,它保证以前的评估的所有效果都已经执行过。说i = ++i + 1未定义,因为它递增i并且还将i分配给它自己,这两者都不是单独定义的序列点。因此,未指明首先会发生这种情况。

答案 3 :(得分:10)

C ++ 11更新(09/30/2011)

停止,这在C ++ 11中是定义良好。它仅在C ++ 03中未定义,但C ++ 11更灵活。

int i = 0;
i = ++i + 1;

在那一行之后,i将是2.这个改变的原因是......因为它已经在实践中起作用,并且要使其未定义而不是仅仅将它定义为C ++ 11的规则(实际上,现在这种情况比故意改变更容易发生,所以 在你的代码中执行它!)

直接从马的嘴里

http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#637

答案 4 :(得分:9)

给出两个选择:已定义或未定义,您将选择哪个?

标准的作者有两种选择:定义行为或将其指定为未定义。

鉴于首先编写此类代码的明显不明智的性质,为其指定结果没有任何意义。人们会想要阻止这样的代码,而不是鼓励它。对任何事情都没有用处或必要。

此外,标准委员会没有任何办法迫使编译器编写者做任何事情。如果他们需要特定的行为,那么该要求可能会被忽略。

也有实际的原因,但我怀疑他们从属于上述一般考虑因素。但是对于记录来说,这种表达式和相关类型的任何必需行为都会限制编译器生成代码,分解常见子表达式,在寄存器和内存之间移动对象等的能力.C已被弱可见性所阻碍限制。像Fortran这样的语言很久以前就意识到别名参数和全局变量是一个优化杀手,我相信他们只是禁止它们。

我知道你对一个特定的表达感兴趣,但任何给定结构的确切性质并不重要。预测复杂的代码生成器将会做什么并且语言试图在愚蠢的情况下不需要这些预测并不容易。

答案 5 :(得分:8)

标准的重要部分是:

  

通过评估表达式

,其存储值最多修改一次

您可以将值修改两次,一次使用++运算符,一次使用赋值

答案 6 :(得分:7)

请注意,您的标准副本已过时,并且在示例的第1和第3个代码行中包含已知(且已修复)的错误,请参阅:

C++ Standard Core Language Issue Table of Contents, Revision 67, #351

Andrew Koenig: Sequence point error: unspecified or undefined?

这个主题并不容易只是阅读标准(这是非常模糊的:(在这种情况下)。

例如,它是否良好(或未定义),未指定,或者通常情况下实际上不仅取决于语句结构,还取决于执行时的内存内容(具体来说,变量值) ,另一个例子:

++i, ++i; //ok

(++i, ++j) + (++i, ++j); //ub, see the first reference below (12.1 - 12.3)

请看一下(它清楚而准确):

JTC1/SC22/WG14 N926 "Sequence Point Analysis"

此外,Angelika Langer有一篇关于这个主题的文章(尽管不像前一篇那样清晰):

"Sequence Points and Expression Evaluation in C++"

还有一个俄语讨论(尽管在评论和帖子中有一些明显错误的陈述):

"Точки следования (sequence points)"

答案 7 :(得分:4)

假设您在问“为什么语言是这样设计的?”。

您说i = ++i + i“显然未定义”,但i = ++i + 1应该i保留定义的值?坦率地说,这不会很一致。我更喜欢完全定义所有内容,或者一切都未指定。在C ++中我有后者。这本身并不是一个非常糟糕的选择 - 首先,它阻止你编写邪恶的代码,在同一个“语句”中进行五或六次修改。

答案 8 :(得分:4)

以下代码演示了如何获得错误(意外)结果:

int main()
{
  int i = 0;
  __asm { // here standard conformant implementation of i = ++i + 1
    mov eax, i;
    inc eax;
    mov ecx, 1;
    add ecx, eax;
    mov i, ecx;

    mov i, eax; // delayed write
  };
  cout << i << endl;
}

结果会打印1。

答案 9 :(得分:3)

类比论证:如果你认为运算符是函数的类型,那么它是有道理的。如果你有一个重载operator=的类,你的赋值语句将等同于:

operator=(i, ++i+1)

(第一个参数实际上是通过this指针隐式传递的,但这只是为了说明。)

对于普通函数调用,这显然是未定义的。第一个参数的值取决于何时计算第二个参数。但是对于原始类型,你可以使用它,因为i的原始值只是被覆盖了;它的价值无关紧要。但如果你在自己的operator=中做了其他一些魔术,那么差异可能会浮出水面。

简单地说:所有操作符都像函数一样,因此应该按照相同的概念运行。如果i + ++i未定义,则i = ++i也应该未定义。

答案 10 :(得分:2)

怎么样,我们都同意永远不会写这样的代码?如果编译器不知道你想做什么,那么你如何期待跟在你后面的糟糕闷棍知道你想做什么?放i ++;在它自己的行上将杀死你。

答案 11 :(得分:1)

  

i = v [i ++]; //行为未指明
  i = ++ i + 1; //行为未指明为

以上所有表达式都会调用未定义的行为。

  

i = 7,i ++,i ++; //我变成了9

这很好。

阅读Steve Summit的C-FAQs。

答案 12 :(得分:1)

根本原因是编译器处理值的读写方式。允许编译器在内存中存储中间值,并且实际上只在表达式的末尾提交值。我们将表达式++i读作“增加i一个并返回”,但编译器可能会将其视为“加载i的值,添加一个,返回它,以及在有人再次使用之前将其提交回内存。鼓励编译器尽可能避免读/写实际的内存位置,因为这会降低程序的速度。

i = ++i + 1的特定情况下,它主要由于需要一致的行为规则而受到影响。在这种情况下,许多编译器会做“正确的事情”,但如果其中一个i实际上是一个指向i的指针呢?如果没有这个规则,编译器必须非常小心,以确保它以正确的顺序执行加载和存储。此规则可用于提供更多优化机会。

类似的情况是所谓的严格别名规则。您无法通过不相关类型的值(例如,int)分配值(例如,float),只有少数例外情况。这使编译器不必担心使用的float *会改变int的值,并大大提高优化潜力。

答案 13 :(得分:1)

这里的问题是标准允许编译器在语句执行时完全重新排序。但是,不允许对语句重新排序(只要任何此类重新排序导致程序行为发生变化)。因此,表达式i = ++i + 1;可以通过两种方式进行评估:

++i; // i = 2
i = i + 1;

i = i + 1;  // i = 2
++i;

i = i + 1;  ++i; //(Running in parallel using, say, an SSE instruction) i = 1

当你在混合中抛出用户定义的类型时,这会变得更糟,其中++运算符可以对类型的作者想要的类型产生任何影响,在这种情况下,评估中使用的顺序非常重要。

答案 14 :(得分:0)

++i开始,我必须分配“1”,但是i = ++i + 1必须为其分配值“2”。由于没有中间序列点,编译器可以假设相同的变量没有被写入两次,因此这两个操作可以按任何顺序完成。所以是的,如果最终值为1,编译器将是正确的。