关于以下代码是否是合法的C++ ,这个问题一直存在争议:
std::list- ::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 ++)之前还是之后都依赖于编译器."
我打开这个问题,希望能够解决这个问题.
答曰C++标准 16年9月1日:
当调用函数时(无论函数是否为内联函数),与任何参数表达式相关联的每个值计算和副作用,或者使用指定被调用函数的后缀表达式,都会在执行每个表达式或语句之前对其进行排序.叫功能.(注意:与不同参数表达式关联的值计算和副作用未被排序.)
所以在我看来这段代码:
foo(i++);
是完全合法的.它将递增i
,然后foo
使用之前的值调用i
.但是,这段代码:
foo(i++, i++);
产生未定义的行为,因为第1.9.16段也说:
如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,则行为未定义.
以克里斯托的答案为基础,
foo(i++, i++);
产生未定义的行为,因为计算函数参数的顺序是未定义的(在更一般的情况下,因为如果您在表达式中读取变量两次,您也会在其中编写它,结果是未定义的).您不知道哪个参数将首先递增.
int i = 1; foo(i++, i++);
可能会导致函数调用
foo(2, 1);
要么
foo(1, 2);
甚至
foo(1, 1);
运行以下命令以查看平台上发生的情况:
#includeusing 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- ::iterator i_before = i; i = i_before + 1; items.erase(i_before);
而不是:
std::list- ::iterator i_before = i; items.erase(i); i = i_before + 1;
因此在这种情况下它是安全的,因为list.erase()特别不会使除擦除之外的任何迭代器无效.
也就是说,它的样式很糟糕 - 所有容器的擦除函数都会返回下一个迭代器,因此您不必担心由于重新分配而使迭代器失效,因此惯用代码:
i = items.erase(i);
对于列表是安全的,如果您想更改存储空间,对于矢量,deques和任何其他序列容器也是安全的.
您也不会在没有警告的情况下编译原始代码 - 您必须编写
(void)items.erase(i++);
避免关于未使用的回报的警告,这将是你做一些奇怪事情的一个重要线索.