为什么“我= ++ i + 1”未指定的行为?
考虑以下C ++标准ISO / IEC 14882:2003(E)引用(第5节,第4段):
除非另有说明,否则个体运算符的操作数和个别expression式的子expression式的评估顺序以及副作用发生的顺序是未指定的。 53)在前一个序列点和下一个序列点之间,一个标量对象应该通过评估一个expression式来最多修改一次标量对象的存储值。 此外,只有在确定要存储的值时才能访问先前值。 对于一个完整expression式的子expression式的每个可允许的sorting,应满足本段的要求; 否则行为是不确定的。 [例:
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
– 例子]
我很惊讶, i = ++i + 1
给i = ++i + 1
一个未定义的值。 有没有人知道一个编译器的实现不给2
以下情况?
int i = 0; i = ++i + 1; std::cout << i << std::endl;
事情就是那个operator=
有两个参数。 第一个总是i
参考。 评估顺序在这种情况下并不重要。 除了C ++ Standard禁忌之外,我没有看到任何问题。
请不要考虑争论顺序对评估重要的情况。 例如, ++i + i
显然是未定义的。 请只考虑我的情况i = ++i + 1
。
为什么C ++标准禁止这样的expression式?
你认为operator=
的错误是一个双参数函数 ,在函数开始之前必须完整地评估参数的副作用。 如果是这样的话,则expression式i = ++i + 1
将具有多个序列点,并且在分配开始之前++i
将被完全评估。 但事实并非如此。 内在赋值运算符中正在评估哪些内容,而不是用户定义的运算符。 该expression式中只有一个序列点。
++i
的结果在赋值之前(和加法运算符之前)被评估,但副作用不一定是马上应用的。 ++i + 1
的结果总是与i + 2
相同,所以这是作为赋值运算符的一部分赋值给i
的值。 ++i
的结果总是i + 1
,所以这就是作为增量运算符的一部分被赋值给i
的东西。 没有顺序点来控制首先应该分配哪个值。
由于代码违反了“在前一个和下一个序列点之间标量对象的存储值最多一次通过评估expression式被修改”的规则,所以该行为是不确定的。 不过实际上 ,很可能先分配i + 1
或i + 2
,然后分配另一个值,最后程序会像往常一样继续运行 – 没有恶魔或爆炸的厕所,没有i + 3
,要么。
这是未定义的行为,而不是(仅)未指定的行为,因为有两个写入i
没有介入序列点。 就标准规定而言,这是按照定义的方式。
该标准允许编译器生成延迟回写到存储的代码 – 或者从另一个angular度来看,重新执行实现副作用的指令 – 只要符合序列点的要求,就可以select任何方式。
这个语句expression式的问题在于,它隐含了两个写入i
没有干预序列点:
i = i++ + 1;
一次写入的是i
的原始值的值“加一”,另一次写入的值是“加一”。 只要标准允许,这些写操作可以按任何顺序发生或完全炸毁。 理论上,这甚至可以让执行自由地并行执行写回操作,而不用麻烦检查同时访问错误。
C / C ++定义了一个叫做序列点 ( sequence points)的概念,这个概念引用了一个执行点,保证之前评估的所有效果都已经被执行。 说i = ++i + 1
是未定义的,因为它增加i
,并且将i
赋给它自己,这两者都不是单独定义的序列点。 因此,没有具体说明哪一个会先发生。
更新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
给定两个select:定义或不定义,你会做出哪个select?
该标准的作者有两个select:定义行为或将其指定为未定义。
鉴于编写这样的代码明显不明智的本质,为它指定结果没有任何意义。 人们会想阻止这样的代码,而不是鼓励它。 这对于任何事情都没有用或不必要。
而且,标准委员会也没有办法强制编译器编写者做任何事情。 如果他们需要一个特定的行为,这个要求可能会被忽略。
也有实际的原因,但我怀疑他们是从属于上述的一般考虑。 但是为了logging,这种expression式和相关types所需的任何行为都会限制编译器生成代码,分解公共子expression式,在寄存器和内存之间移动对象等的能力。C已经被弱可见性限制。 像Fortran这样的语言很早就意识到别名参数和全局variables是一个优化杀手,我相信他们只是禁止它们。
我知道你对一个具体的expression感兴趣,但是任何给定构造的确切性质并不重要。 要预测一个复杂的代码生成器将会做什么并且在愚蠢的情况下语言试图不需要这些预测是不容易的。
标准的重要部分是:
其存储值最多一次通过评估expression式而被修改
您可以使用++运算符两次修改该值,一次使用赋值
请注意,您的标准副本已过时,并在您的示例的第一行和第三行代码中包含已知(并且是固定的)错误,请参阅:
C ++标准核心语言问题目录,修订版67,#351
和
Andrew Koenig:序列点错误:未指定或未定义?
这个话题不容易得到只读标准(这是非常模糊的:(在这种情况下)。
例如,它可以很好地(或不是)定义,也可以不指定,或者在一般情况下实际上不仅取决于语句结构,还取决于执行时的内存内容(具体而言,variables值),另一个例子:
++i, ++i; //ok (++i, ++j) + (++i, ++j); //ub, see the first reference below (12.1 - 12.3)
请看看(它有清楚而准确的):
JTC1 / SC22 / WG14 N926“序列点分析”
另外,Angelika Langer也有一篇关于这个话题的文章(虽然不像前一篇那么清楚):
“C ++中的序列点和expression式评估”
还有一个在俄罗斯的讨论(尽pipe在评论和邮件本身有一些明显的错误陈述):
“Точкиследования(sequence points)”
以下代码演示如何得到错误的(意外的)结果:
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结果。
假设你问“为什么用这种方式devise语言?”。
你说i = ++i + i
是“明显未定义”,但i = ++i + 1
应离开i
一个定义的值? 坦率地说,这不会很一致。 我更喜欢把所有的东西都完美的定义,或者一直没有指定的东西。 在C + +我有后者。 这本身并不是一个非常糟糕的select – 一方面,它阻止了你编写在同一个“陈述”中进行五六次修改的邪恶代码。
类比的论点:如果你把操作符看作是函数的types,那么它是有道理的。 如果你有一个重载operator=
的类,你的赋值语句就是这样的:
operator=(i, ++i+1)
(第一个参数实际上是通过this
指针隐式传递的,但这只是为了说明。)
对于普通的函数调用,这显然是不确定的。 第一个参数的值取决于何时评估第二个参数。 然而,对于原始types,由于原来的值被简单覆盖, 其价值并不重要。 但是,如果你在自己的operator=
身上做了一些其他的魔法,那么差异就会浮出水面。
简而言之:所有的操作员都像function一样,因此应该按照相同的概念行事。 如果i + ++i
是未定义的,那么i = ++i
应该是未定义的。
怎么样,我们都同意永远不写这样的代码? 如果编译器不知道你想要做什么,那么你如何期待在你身后的可怜的sap理解你想要做什么? 把i ++; 在自己的线上不会杀了你。
其根本原因是由于编译器处理值的读写操作。 允许编译器在内存中存储一个中间值,并且只在expression式的末尾实际提交该值。 我们把expression式++i
看成是“增加一个并返回它”,但编译器可能会认为它是“加载i
的值,添加一个,返回它,并提交回内存之前有人再次使用它鼓励编译器尽可能避免读取/写入实际的存储器位置,因为这会降低程序的速度。
在i = ++i + 1
的具体情况下,主要是由于需要一致的行为规则。 许多编译器在这种情况下会做“正确的事情”,但是如果其中一个实际上是一个指针,指向i
呢? 没有这个规则,编译器必须非常小心地确保它以正确的顺序执行加载和存储。 这个规则可以提供更多的优化机会。
类似的情况就是所谓的严格别名规则。 你不能通过一个不相关的types(比如,一个float
)来赋值(比如int
),只有less数例外。 这使编译器不必担心一些使用的float *
会改变int
的值,并极大地提高了优化的可能性。
这里的问题是标准允许编译器在执行时完全重新排列语句。 但是,不允许对语句重新sorting(只要任何这样的重新sorting导致程序行为改变)。 因此,expression式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
当用户定义types在组合中抛出时,情况会变得更糟,其中++运算符可以对types作者想要的types产生任何影响,在这种情况下,评估中使用的顺序非常重要。
我= v [i ++]; //行为是未指定的
i = ++ i + 1; //行为是未指定的
以上所有expression式都会调用未定义的行为。
i = 7,i ++,i ++; //我变成了9
这可以。
阅读Steve Summit的C-FAQ。
从++i
,我必须分配“1”,但是在i = ++i + 1
,必须赋值“2”。 由于没有中间顺序点,因此编译器可以假定同一个variables没有被写入两次,所以这两个操作可以以任何顺序完成。 所以是的,如果最终值是1,编译器将是正确的。