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

Ale*_*tov 44 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 
Run Code Online (Sandbox Code Playgroud)

- 末端的例子]

我很惊讶,i = ++i + 1给出了一个未定义的值i.有没有人知道编译器实现不能给出2以下情况?

int i = 0;
i = ++i + 1;
std::cout << i << std::endl;
Run Code Online (Sandbox Code Playgroud)

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

,不要考虑这样的情况下,参数的顺序是评价非常重要.例如,++i + i显然是未定义的.请,只考虑我的情况 i = ++i + 1.

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

Rob*_*edy 62

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

结果++i分配(和加法运算符之前),但前被评估副作用不一定施加马上.结果++i + 1总是相同的i + 2,因此这是i作为赋值运算符的一部分赋值的值.结果++i总是i + 1如此,因此i作为增量运算符的一部分被赋值.没有序列点来控制首先分配哪个值.

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

  • +1"没有鼻子恶魔或爆炸的厕所" (5认同)
  • @nalply:如果[高科技]厕所垃圾处理系统的固件在某处获得'2'而不是'1'作为压力系数? (4认同)
  • 我认为这个答案最直接地解决了OP的混乱来源.由于`i`是一个整数类型(与具有重载`=`运算符的类相对),因此`operator =`没有序列点,因为`operator =`不是函数调用. (3认同)

CB *_*ley 37

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

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

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

i = i++ + 1;
Run Code Online (Sandbox Code Playgroud)

一次写入是原始值i"加一"的值,另一次写入是"加一"的值.这些写入可以按任何顺序发生,也可以在标准允许的范围内完全爆炸.从理论上讲,这甚至使实现可以自由地并行执行回写,而无需检查同时访问错误.

  • 是.这是未定义的.在执行这行代码时,您的计算机可能会变成棉花糖. (8认同)
  • @GMan:在执行`i = i ++ + 1`时,将你的计算机变成棉花糖的C实现确实与标准完全一致,但我认为这是一个实施问题的质量,并且会非常不满意实施,除非涉及巧克力. (6认同)
  • 它的实现已定义.我们这里不做意见.:)如果使用它们编译的代码符合标准,编译器可以称为符合标准.因为这是未定义的行为,编译器可以做任何他们想要的*.在将"i"设置为123456789之前,他们可以重新格式化您的计算机. (5认同)
  • 延迟写入不是标准中明确提到的,它们是实现细节.如果您查看标准指定的所有要求,并且您可以设计一个符合要求的实现,那么只要格式良好的程序在运行时具有指定的行为,它就无关紧要. (5认同)

Cha*_*via 15

C/C++定义了一个称为序列点的概念,它指的是执行中的一个点,它保证了先前评估的所有效果都已经执行过.说i = ++i + 1是未定义的,因为它递增i并且也分配i给它自己,它们都不是单独定义的序列点.因此,未指明哪个将首先发生.

  • @Andreas:这并没有使这个信息回答问题的事实无效.这个问题包含了自己的答案. (8认同)
  • 所有这些信息都已在问题中说明 (3认同)
  • @Daniel我的解释是Alexsey已经知道这一点,但想知道为什么标准是按原样编写的. (2认同)

Joh*_*itb 10

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

停止,这在C++ 11中得到了很好的定义.它仅在C++ 03中未定义,但C++ 11更灵活.

int i = 0;
i = ++i + 1;
Run Code Online (Sandbox Code Playgroud)

在那一行之后,i将是2.这个改变的原因是......因为它已经在实践中起作用,并且如果将其定义为在C++ 11的规则中定义它将是更多的工作是不确定的(实际上,现在这种做法更像是一次意外而非故意改变,所以 不要在你的代码中这样做!).

直接从马的嘴里

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


Dig*_*oss 9

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

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

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

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

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

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


Tre*_*ent 8

该标准的重要部分是:

通过表达式的计算,其存储值最多被修改一次

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

  • @Alexey,你问为什么标准说它未定义?我不知道答案,你将不得不问标准委员会的人. (3认同)

mlv*_*ljr 7

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

C++标准核心语言问题目录,修订版67,#351

Andrew Koenig:序列点错误:未指定或未定义?

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

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

++i, ++i; //ok

(++i, ++j) + (++i, ++j); //ub, see the first reference below (12.1 - 12.3)
Run Code Online (Sandbox Code Playgroud)

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

JTC1/SC22/WG14 N926"序列点分析"

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

"C++中的序列点和表达式评估"

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

"Точкиследования(序列点)"