在C ++函数调用中使用increment运算符是否合法?

在这个问题上,关于下面的代码是否是合法的C ++有一些争论:

std::list<item*>::iterator i = items.begin(); while (i != items.end()) { bool isActive = (*i)->update(); if (!isActive) { items.erase(i++); // *** Is this undefined behavior? *** } else { other_code_involving(*i); ++i; } } 

这里的问题是, erase()会使有问题的迭代器失效。 如果这发生在i++被评估之前,那么就像这样增加i技术上是未定义的行为,即使它似乎与一个特定的编译器一起工作。 辩论的一面说,在函数被调用之前,所有的函数参数都被完全评估。 另一方面说:“唯一的保证是i ++会在下一个语句之前和i ++被使用之后发生,无论是在擦除之前(i ++)被调用还是之后的编译器都依赖于它。

我打开这个问题希望能够解决这个问题。

Quoting的C ++标准 1.9.16:

当调用一个函数(函数是否是内联函数)时,在任何参数expression式或者指定被调用函数的后缀expression式的每一个值计算和副作用在执行每个expression式或者语句称为function。 (注意:与不同参数expression式相关的值计算和副作用是不确定的。)

所以在我看来,这个代码:

 foo(i++); 

是完全合法的。 它会增加i ,然后用前面的值i调用foo 。 但是,这个代码:

 foo(i++, i++); 

产生未定义的行为,因为1.9.16段也说:

如果对标量对象的副作用相对于同一个标量对象的另一个副作用或者使用相同标量对象的值进行值计算而言是不确定的,则行为是不确定的。

基于克里斯托的回答 ,

 foo(i++, i++); 

会产生未定义的行为,因为函数参数的求值顺序是未定义的(在更一般的情况下,因为如果在写入它的expression式中读取两次variables,则结果是未定义的 )。 你不知道哪个参数会首先被增加。

 int i = 1; foo(i++, i++); 

可能会导致一个函数调用

 foo(2, 1); 

要么

 foo(1, 2); 

甚至

 foo(1, 1); 

运行以下命令查看您的平台上会发生什么情况:

 #include <iostream> using namespace std; void foo(int a, int b) { cout << "a: " << a << endl; cout << "b: " << b << endl; } int main() { int i = 1; foo(i++, i++); } 

在我的机器上,我得到了

 $ ./a.out a: 2 b: 1 

每一次,但这个代码是不可移植的 ,所以我希望看到与不同的编译器不同的结果。

该标准说,副作用发生在通话之前,所以代码是一样的:

 std::list<item*>::iterator i_before = i; i = i_before + 1; items.erase(i_before); 

而不是:

 std::list<item*>::iterator i_before = i; items.erase(i); i = i_before + 1; 

所以在这种情况下是安全的,因为list.erase()并不会使除擦除之外的任何迭代器失效。

也就是说,它的风格很糟糕 – 所有容器的清除函数都会专门返回下一个迭代器,所以您不必担心由于重新分配而使迭代器无效,所以惯用代码:

 i = items.erase(i); 

对列表来说是安全的,而且如果你想改变你的存储空间,对于vector,deques和其他序列容器也是安全的。

你也不会得到原始代码没有警告编译 – 你必须写

 (void)items.erase(i++); 

以避免一个关于未使用的回报的警告,这将是一个很大的线索,你做一些奇怪的事情。

完全没问题 通过的值将是增量前的“i”的值。

build立在MarkusQ的答案上:;)

或者说,比尔对此的评论:

编辑:哦,评论又一次了…哦)

他们被允许进行平行评估。 在实践中是否发生在技术上是不相关的。

你不需要线程的并行性,只要在第二步之前(增量i)评估两者的第一步(取i的值)。 完全合法的,一些编译器可能会认为它比第二个开始之前充分评估一个i ++更有效。

事实上,我希望这是一个普遍的优化。 从指令调度的angular度来看它。 你有以下你需要评估:

  1. 以正确的论据的我的价值
  2. 增加我在正确的论点
  3. 拿左边的参数来看我的价值
  4. 在左边的参数中增加i

但是左右参数之间确实没有依赖关系。 参数评估以未指定的顺序进行,不需要按顺序执行(这就是为什么函数参数中的new()通常是内存泄漏,即使包装在智能指针中也是如此)还未定义当修改相同的variables时会发生什么两次相同的expression。 我们确实有一个1和2之间的依赖关系,然而,3和4之间。为什么编译器等待2完成之前计算3? 这引入了额外的延迟,并且在4变得可用之前花费的时间甚至超过了必要的时间。 假设每个周期之间有1个周期的延迟,从1完成需要3个周期,直到4的结果准备就绪,我们可以调用该函数。

但是如果我们重新排列它们并按照第1,3,2,4的顺序进行评估,我们可以在2个周期内完成。 1和3可以在同一个循环中启动(甚至可以合并成一个指令,因为它是相同的expression式),并且在下面,可以评估2和4。 所有现代的CPU每个周期可以执行3-4条指令,一个好的编译器应该尝试利用它。

++克里斯托!

关于如何为一个类实现operator ++(postfix),C ++标准1.9.16是非常有意义的。 当调用operator ++(int)方法时,它会自行增加并返回原始值的一个副本。 正如C ++规范所说的那样。

很高兴看到标准的改进!


不过,我清楚地记得使用旧的(pre-ANSI)C编译器,其中:

 foo -> bar(i++) -> charlie(i++); 

没有做你的想法! 相反,它编译等价于:

 foo -> bar(i) -> charlie(i); ++i; ++i; 

而这种行为是依赖于编译器实现的。 (使移植有趣)


testing和validation现代编译器现在是否正确:

 #define SHOW(S,X) cout << S << ": " # X " = " << (X) << endl struct Foo { Foo & bar(const char * theString, int theI) { SHOW(theString, theI); return *this; } }; int main() { Foo f; int i = 0; f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i); SHOW("END ",i); } 


响应线程中的评论…

…build立在几乎每个人的答案…(谢谢你们!)


我认为我们需要把这个好一些:

鉴于:

 baz(g(),h()); 

那么我们不知道是否 h() 之前或之后调用g () 。 这是“未指定”

但是我们知道g()h()会在baz()之前调用

鉴于:

 bar(i++,i++); 

再一次,我们不知道会先评估哪个i ++ ,也许甚至不知道在bar()被调用之前是否会增加一次或两次。 结果是不确定的! (给定我= 0 ,这可能是酒吧(0,0)酒吧(1,0)酒吧(0,1)或真的很奇怪!)


鉴于:

 foo(i++); 

我们现在知道,在foo()被调用之前, 会被增加。 正如Kristo从C ++标准第1.9.16节中指出的那样:

当调用一个函数(函数是否是内联函数)时,在任何参数expression式或者指定被调用函数的后缀expression式的每一个值计算和副作用在执行每个expression式或者语句称为function。 [注:与不同参数expression式相关的值计算和副作用是不确定的。 – 结束注意]

虽然我认为第5.2.6节说得更好:

后缀++expression式的值是其操作数的值。 [注意:所获得的值是原始值的一个副本 – 结束注释]操作数应该是一个可修改的左值。 操作数的types应为算术types或指向完整有效对象types的指针。 操作数对象的值通过加1来修改,除非对象是booltypes,在这种情况下它被设置为true。 [注:此用法已弃用,请参阅附录D. – 注释]在对操作数对象进行修改之前,++expression式的值计算是按顺序排列的。 对于一个不确定sorting的函数调用,postfix ++的操作是一个单独的评估。 [注意:因此,函数调用不应干涉左值到右值转换和与任何单个后缀++运算符相关的副作用。 – 结束注释]结果是一个右值。 结果的types是操作数types的cv不合格版本。 另见5.7和5.17。

第1.9.16节中的标准也列出(作为其例子的一部分):

 i = 7, i++, i++; // i becomes 9 (valid) f(i = -1, i = -1); // the behavior is undefined 

我们可以用下面的方法来certificate这一点:

 #define SHOW(X) cout << # X " = " << (X) << endl int i = 0; /* Yes, it's global! */ void foo(int theI) { SHOW(theI); SHOW(i); } int main() { foo(i++); } 

所以,是的, foo()被调用之前递增。


所有这些从以下angular度来看都是非常有意义的:

 class Foo { public: Foo operator++(int) {...} /* Postfix variant */ } int main() { Foo f; delta( f++ ); } 

这里Foo :: operator ++(int)必须在delta()之前被调用。 增量操作必须在调用过程中完成。


在我(也许是过于复杂)的例子中:

 f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i); 

必须执行f.bar(“A”,i)才能获得object.bar(“B”,i ++)所用的对象, 以此类推 “C”“D”

所以我们知道i ++在调用bar(“B”,i ++)之前增加了i (即使bar(“B”,…)被调用了旧的i值),因此ibar “C”,i)bar(“D”,i)


回到j_random_hacker的评论:

j_random_hacker写道:
+1,但我必须仔细阅读标准来说服自己,这是好的。 我正确地认为,如果bar()是一个返回int的全局函数,f是一个int,并且这些调用通过“^”而不是“。”连接,那么A,C和D中的任何一个都可以举报“0”?

这个问题比你想象的要复杂得多

重写你的问题作为代码…

 int bar(const char * theString, int theI) { SHOW(...); return i; } bar("A",i) ^ bar("B",i++) ^ bar("C",i) ^ bar("D",i); 

现在我们只有一个expression。 根据标准(第1.9页,第8页,pdf第20页):

(7)例如,在下面的片段中:a = a + 32760 + b + 5;注意:运算符可以根据通常的math规则重新组合。 expression式语句的行为完全相同:a =(((a + 32760)+ b)+5); 由于这些运营商的关联性和优先级。 因此,总和(a + 32760)的结果接下来被加到b,然后该结果被加到5上,结果赋值给a。 在一个溢出产生exception的机器中,一个int表示的值的范围是[-32768,+ 32767],实现不能把这个expression式重写为a =((a + b)+32765)。 因为如果a和b的值分别是-32754和-15,那么a + b的总和就会产生一个exception,而原始的expression式不会; expression式也不能被重写为=((a + 32765)+ b); 或a =(a +(b + 32765)); 因为a和b的值可能分别是4和-8或-17和12. 但是,在溢出不产生exception并且溢出结果是可逆的机器上,上述expression式语句可以以上述任何一种方式的实现都会被重写,因为会出现相同的结果。 – 结束注意]

所以我们可能会认为,由于优先,我们的expression将是相同的:

 ( ( ( bar("A",i) ^ bar("B",i++) ) ^ bar("C",i) ) ^ bar("D",i) ); 

但是,由于(a ^ b)^ c == a ^(b ^ c)没有任何可能的溢出情况,因此可以按任意顺序重写…

但是,因为bar()正在被调用,而且可能假设有副作用,所以这个expression式不能以任何顺序重写。 优先规则仍然适用。

这很好地决定了bar()的评估顺序。

现在,那个i + = 1什么时候发生? 那么它在调用bar(“B”,…)之前还是会发生。 (即使bar(“B”,…)被调用旧值)。

所以它确定地发生在条(C)条(D)之前 ,以及条(A)之后

答:没有如果编译器符合标准 ,我们将始终具有“A = 0,B = 0,C = 1,D = 1”


但考虑另一个问题:

 i = 0; int & j = i; R = i ^ i++ ^ j; 

R的价值是什么?

如果i + 1发生在j之前,我们就有0 ^ 0 ^ 1 = 1。 但是如果i + = 1发生在整个expression式之后,我们会有0 ^ 0 ^ 0 = 0。

事实上,R是零。 直到expression式被评估之后, i + = 1才会出现。


我认为这是为什么:

i = 7,i ++,i ++; //我变成了9(有效)

是合法的…它有三个expression式:

  • 我= 7
  • 我++
  • 我++

在每种情况下,每个expression式的结尾都会改变i的值。 (在评估任何后续expression式之前)


PS:考虑:

 int foo(int theI) { SHOW(theI); SHOW(i); return theI; } i = 0; int & j = i; R = i ^ i++ ^ foo(j); 

在这种情况下,必须在foo(j)之前评估i + = 1是1,而R是0 ^ 0 ^ 1 = 1。

蜥蜴的回答build立在比尔的基础上:

 int i = 1; foo(i++, i++); 

也可能导致一个函数调用

 foo(1, 1); 

(这意味着实际值是并行计算的,然后应用后缀)。

– MarkusQ

本周的 Sutter's Guru#55 (和“More Exceptional C ++”中的相应部分)以这个确切的案例作为例子进行讨论。

据他介绍,这是完全有效的代码,事实上是一个试图将该语句转换为两行的情况:

 items.erase(ⅰ);
我++;

不会产生语义上等同于原始语句的代码。