问题 迭代器和标量对象之间的未定义行为有什么不同吗?


话题 关于评估顺序说,以下代码导致未定义的行为,直到C ++ 17:

a[i] = i++;

这是由于在评估赋值表达式的左右部分时未指定的顺序而发生的。

C ++ 14标准1.9 / 15说:

如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,并且它们不是潜在的并发(1.10),则行为未定义。

但是如果我们使用它会怎样 std::vector 和它的 iterator 对象而不是标量对象 i

std::vector<int> v = {1, 2};
auto it = v.begin();
*it = *it++;   // UB?

是否存在未定义的行为(直到c ++ 17)?


6321
2017-08-19 10:21


起源

迭代器是 允许 成为一个原始指针,可能会让你陷入困境。如果它是一个类对象,则运算符是引入序列点的函数调用(在所有标准修订版中)。 - Bo Persson
这特定于向量,其中指针可用作迭代器。在已知的实现上,部分原因是允许调试迭代器存储额外的状态。而且,即使 *it = *it++; 会工作,也许会这样 *it = *it; ++it; 反正并不是很有用。大多数临界代码就是这样 - 很难看到它的作用,并且只是略微有用。 - Bo Persson
如果vector的任何可行实现都有一个迭代器,这会产生未定义的行为,那么行为通常是不确定的。未定义行为的一个可能结果是实现选择,其中结果实际上是明确定义的。 - Peter
我不知道。绝对是 不 介绍一个函数调用 *it++ 在电话会议之前进行全面评估 f。但是,在C ++ 17之前,我怀疑在增量之前或之后仍然可以评估左侧。所以我从不写这样的代码。 - Bo Persson
为了澄清(如果我正确理解这一点),C ++ 17仍然没有指定评估顺序,所以 a[i] = i++; 不是个好主意。 C ++ 17只排除了鼻子恶魔,并保证你会分配 old_i要么 a[old_i] 要么 a[new_i]。可能存在这样的情况:任何一个订单都没有问题,而将其留给编译器来选择就是你想要的。 (例如。 f(++i, ++i) 哪里 f(a,b) 是可交换的。)C ++ 17允许你安全地写。但 n = ++i + i; 在C ++ 17中仍然完全是UB。 - Peter Cordes


答案:


在迭代器是一个类的情况下,行为在标准的所有版本中都有明确定义,假设如此 it++ 指向其容器内的有效位置(在您的示例中它确实如此)。

C ++翻译 *it++ 这两个函数调用的序列:

it.operator++(0).operator*();

函数调用引入了排序,因此实际的所有副作用 ++ 在里面调用 operator++ 用作迭代器实现的原语(可能是原始指针)必须在函数退出之前完成。

但是,迭代器不需要是类:它们也可以是指针:

struct foo {
    typedef int* iterator;
    iterator begin() { return data; }
private:
    int data[10];
};

代码看起来一样,并且继续编译,但现在行为是未定义的:

foo f;
auto it = f.begin();
*it = *it++; // <<== This is UB

您可以通过调用来防范这种情况 ++ 作为会员功能:

std::vector<int> v = {1, 2};
auto it = v.begin();
*it = *it.operator++(0);

当迭代器实际上是一个指针时,此代码将无法编译,而不是导致未定义的行为。


15
2017-08-19 10:41



如果我使用 f(*it++) 在右侧,UB是否会在原始指针实现时消失? - alexolut
@alexolut是的,参数表达式计算的所有副作用在输入函数之前生效。 - dasblinkenlight
难道你不是在假设C ++ 17规则“(复合 - )赋值的左侧是在右侧之后排序吗?当然,由于不确定的排序没有未定义的行为,它只是未指定的行为,但仍然。 - Deduplicator
@dasblinkenlight嗯,但左侧和右侧的评估顺序仍然没有排序。即 *it (lhs)可以在右侧函数调用之前或之后进行评估。因此仍然是UB。我对吗? - alexolut