关于以下代码是否合法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 ++)之前还是之后都是编译器相关的。”
我打开这个问题,希望能够解决这个问题。
答案 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 ++更有效。
事实上,我希望它是一种常见的优化方式。从指令调度的角度来看它。您需要评估以下内容:
但左右争论之间确实没有依赖关系。参数评估以未指定的顺序发生,也不需要顺序完成(这就是为什么函数参数中的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++;
不生成在语义上等同于原始语句的代码。