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

时间:2009-02-28 15:22:45

标签: c++ function standards

关于以下代码是否合法C ++,this question中有一些争论:

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 ++)之前还是之后都是编译器相关的。”

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

8 个答案:

答案 0 :(得分:57)

C++ standard 1.9.16:

  

调用函数时(无论是否为   每个人都不是内联的功能   价值计算和副作用   与任何参数相关联   表达式,或与后缀   表达被称为的表达   功能,之前是排序的   执行每个表达式或   在被召唤的身体中的陈述   功能。 (注意:价值计算   与...相关的副作用   不同的参数表达式   未测序。)

所以在我看来这段代码:

foo(i++);

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

foo(i++, i++);

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

  

如果对标量对象产生副作用   没有相对于另一个人   对同一标量物体的副作用   或者使用该值计算值   同一个标量对象,   行为未定义。

答案 1 :(得分:13)

构建于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

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

答案 2 :(得分:5)

标准说副作用发生在通话之前,因此代码与:

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

对于列表是安全的,如果您想要更改存储,对于矢量,deques和任何其他序列容器也是安全的。

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

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

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

答案 3 :(得分:3)

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

答案 4 :(得分:2)

以MarkusQ的答案为基础:;)

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

编辑:噢,评论再次消失......好吧)

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

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

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

  1. 将i的值作为正确的参数
  2. 在正确的论证中增加i
  3. 取左参数的值
  4. 在左参数中增加i
  5. 但左右争论之间确实没有依赖关系。参数评估以未指定的顺序发生,也不需要顺序完成(这就是为什么函数参数中的new()通常是内存泄漏,即使包装在智能指针中) 它也未定义在同一表达式中两次修改相同变量时会发生什么。 但是,我们确实存在1到2之间的依赖关系,并且介于3和4之间。 那么为什么编译器在计算3之前等待2完成呢?这会增加延迟,并且在4可用之前需要更长的时间。 假设每个之间存在1个周期的延迟,则从1完成需要3个周期,直到4的结果准备就绪,我们可以调用该函数。

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

答案 5 :(得分:2)

答案 6 :(得分:0)

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

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

也可能导致

的函数调用
foo(1, 1);

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

- MarkusQ

答案 7 :(得分:0)

Sutter的Guru of the Week #55(和“More Exceptional C ++”中的相应部分)讨论了这个确切的案例作为例子。

根据他的说法,它是完全有效的代码,实际上是一种尝试将语句转换为两行的情况:

items.erase(i);
i++;

生成在语义上等同于原始语句的代码。