代码之家  ›  专栏  ›  技术社区  ›  Michael Kristofik

在C++函数调用中使用增量运算符合法吗?

  •  49
  • Michael Kristofik  · 技术社区  · 17 年前

    里面有一些争论 this question 关于以下代码是否是合法的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

    我提出这个问题是为了希望解决这场辩论。

    8 回复  |  直到 8 年前
        1
  •  64
  •   Michael Kristofik    17 年前

    C++ standard 1.9.16:

    调用函数时(无论是或 价值计算与副作用 与任何论点相关 表达式,或带有后缀 指定被调用对象的表达式 功能,之前排序 执行每个表达式或 被叫方正文中的声明 功能。(注:价值计算 以及与 不同的参数表达式是

    在我看来,这段代码:

    foo(i++);
    

    完全合法。它将增加 i 然后打电话 foo 与之前的值 然而,此代码:

    foo(i++, i++);
    

    如果标量对象的副作用为 相对于彼此而言,没有顺序 或者使用该值进行值计算 对于同一标量对象 行为未定义。

        2
  •  14
  •   Bill the Lizard    7 年前

    Kristo's answer ,

    foo(i++, i++);
    

    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
    

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

        3
  •  5
  •   Pete Kirkham    17 年前

    标准规定副作用发生在调用之前,因此代码与以下代码相同:

    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);
    

    你也不会在没有警告的情况下编译原始代码——你必须编写

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

    为了避免出现关于未使用回报的警告,这将是一个很大的线索,表明你正在做一些奇怪的事情。

        4
  •  3
  •   Himadri Choudhury    17 年前

    这完全没问题。 传递的值将是增量前的“i”值。

        5
  •  3
  •   Community Mohan Dere    8 年前

    C++标准1.9.16对于如何为类实现运算符++(后缀)非常有意义。当调用运算符++(int)方法时,它会自动递增并返回原始值的副本。正如C++规范所说。


    然而,我清楚地记得使用旧的(ANSI之前的)C编译器,其中:

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

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

    这种行为取决于编译器的实现。(让移植变得有趣。)


    很容易测试和验证现代编译器现在的行为是否正确:

    #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);
    }
    


    回复帖子中的评论。..

    …而且几乎在很大程度上 每个人的 答案。..(谢谢大家!)


    我认为我们需要更好地说明这一点:

    鉴于:

    baz(g(),h());
    

    那么我们不知道是否 g() 将被调用 比前后 h() .

    但我们确实知道 两者 g() h() 将在之前调用 baz() .

    鉴于:

    bar(i++,i++);
    

    再一次,我们不知道是哪一个 我++ 将首先进行评估,甚至可能不会 将在之前递增一次或两次 被称为。 结果未定义! (给定 i=0 ,这可能是 巴(0,0) 巴(1.0) 或者真的很奇怪!)


    鉴于:

    foo(i++);
    

    我们现在知道了 将在之前递增 foo() 被调用。像 Kristo 指出从 the C++ standard section 1.9.16:

    后缀++表达式的值是其操作数的值。[注意:获得的值是原始值的副本--结束注释]操作数应为可修改的左值。操作数的类型应为算术类型或指向完整有效对象类型的指针。通过向操作数对象添加1来修改操作数对象的值,除非该对象的类型为bool,在这种情况下,它被设置为true。[注意:此用法已被弃用,请参阅附件D.--尾注]++表达式的值计算在修改操作数对象之前进行排序。 对于一个不确定顺序的函数调用,后缀++的操作是一个单一的求值。[注意:因此,函数调用不应干预左值到右值的转换和与任何单个后缀++运算符相关的副作用。--end Note]

    该标准第1.9.16节还列出了(作为示例的一部分):

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

    我们可以用以下方式轻松地证明这一点:

    #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() 被调用。


    从以下角度来看,这一切都很有意义:

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

    这里 Foo::运算符++(int) 必须在之前调用 delta() 。增量操作必须在调用期间完成。


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

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

    必须执行以获取用于的对象 object.bar(“B”,i++) 等等 C D .

    我++ 打电话之前 (尽管 bar(“B”,…) 用旧值调用 ),因此 在之前递增 bar(“C”,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);
    

    现在我们只有 表达。根据标准(第8页第1.9节,pdf第20页):

    注意:只有当运算符确实是结合的或可交换的时,才能根据通常的数学规则对运算符进行重新分组。(7) 例如,在以下片段中:a=a+32760+b+5;表达式语句的行为与以下内容完全相同:a=(((a+32760)+b)+5);由于这些运算符的关联性和优先级。因此,接下来将总和(a+32760)的结果加到b上,然后将该结果加到5上,得到分配给a的值。在溢出产生异常并且int可表示的值范围为[-32768,+32767]的机器上,实现不能将此表达式重写为a=((a+b)+32765);因为如果a和b的值分别为-32754和-15,则a+b之和将产生异常,而原始表达式不会产生异常;表达式也不能改写为a=((a+32765)+b);或a=(a+(b+32765));因为a和b的值可能分别是4和-8或-17和12。 然而,在溢出不产生异常并且溢出结果可逆的机器上,上述表达式语句可以通过上述任何一种方式由实现重写,因为会出现相同的结果。--结束注释]

    因此,我们可能会认为,由于优先级,我们的表达式将与以下表达式相同:

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

    但是,因为(a^b)^c==a^(b^c)没有任何可能的溢出情况,所以它可以按任何顺序重写。..

    但是,由于bar()正在被调用,并且可能涉及副作用,因此这个表达式不能以任何顺序重写。优先规则仍然适用。

    bar() .

    i+=1 发生?好吧,这还需要以前发生 bar(“B”,…) 被调用。(尽管 用旧值调用。)

    所以,它以前肯定会发生 bar(C) bar(D) ,以及之后 bar(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

    事实上,R是零。这 i+=1


    我想这就是原因:

    i=7,i++,i++;//i变为9(有效)

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

    在每种情况下 在每个表达式结束时发生变化。(在计算任何后续表达式之前。)


    PS:考虑:

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

    i+=1 必须事先进行评估 . 香港高等科技教育学院 R

        6
  •  1
  •   Stack Overflow is garbage    17 年前

    以MarkusQ的答案为基础:;)

    或者更确切地说,比尔对此的评论:

    编辑: 啊,评论又消失了。..好吧)

    他们是 允许 将同时进行评估。从技术上讲,它是否在实践中发生无关紧要。

    事实上,我希望这是一个常见的优化。从指令调度的角度来看。您需要评估以下内容:

    1. 将i的值作为正确的论点
    2. 取左边参数的i值

    但左翼和右翼的论点之间真的没有依赖性。参数求值以未指定的顺序进行,也不需要按顺序进行(这就是为什么函数参数中的new()通常是内存泄漏,即使被包裹在智能指针中) 当你在同一个表达式中两次修改同一个变量时,会发生什么,这也是未定义的。 然而,我们确实在1和2之间以及3和4之间有依赖关系。 那么,编译器为什么要在计算3之前等待2完成呢?这引入了额外的延迟,在4可用之前,需要的时间甚至比必要的时间还要长。 假设每个周期之间有1个周期的延迟,那么从1完成到4的结果准备就绪,我们需要3个周期才能调用函数。

    但是,如果我们对它们进行重新排序,并按照1、3、2、4的顺序进行计算,我们可以在2个周期内完成。1和3可以在同一个循环中启动(甚至合并到一条指令中,因为它是同一个表达式),在下面,2和4可以被求值。

        7
  •  0
  •   Boojum    17 年前

    萨特 Guru of the Week #55 (以及“More Exceptional C++”中的相应部分)以一个例子讨论了这个确切的情况。

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

    items.erase(i);
    i++;
    

    生成语义上与原始语句等效的代码。

        8
  •  -1
  •   MarkusQ    17 年前

    以蜥蜴比尔的回答为基础:

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

    foo(1, 1);
    

    (这意味着并行评估实际值,然后应用postops)。

    --马库斯Q

    推荐文章