如果我有一个迭代器到向量 a
,那么我从 a
移动构造或移动分配向量 b
,该迭代器是否仍指向相同的元素(现在在向量 b
中)?这就是我在代码中的意思:
#include <vector>
#include <iostream>
int main(int argc, char *argv[])
{
std::vector<int>::iterator a_iter;
std::vector<int> b;
{
std::vector<int> a{1, 2, 3, 4, 5};
a_iter = a.begin() + 2;
b = std::move(a);
}
std::cout << *a_iter << std::endl; // Is a_iter valid here?
return 0;
}
a_iter
是否仍然有效,因为 a
已被移入 b
,或者是移动无效的迭代器?供参考, std::vector::swap
does not invalidate iterators .
4 回答
虽然假设
iterator
在move
之后仍然有效可能是合理的,但我不认为标准实际上保证了这一点 . 因此,在move
之后,迭代器处于未定义状态 .我在标准中找不到任何引用,它明确指出
move
之前存在的迭代器在move
之后仍然有效 .从表面上看,假设
iterator
通常被实现为受控序列的指针似乎是完全合理的 . 如果是这种情况,那么迭代器在move
之后仍然有效 .但
iterator
的实现是实现定义的 . 意思是,只要特定平台上的iterator
符合标准规定的要求,它就可以以任何方式实施 . 理论上,它可以实现为返回vector
类的指针和索引的组合 . 如果是这种情况,则move
之后迭代器将变为无效 .iterator
是否实际以这种方式实现是无关紧要的 . 它可以通过这种方式实现,因此如果没有标准的特定保证,那么move
迭代器仍然有效,你不能认为它们是有效的 . 还要记住swap
之后的迭代器有这样的保证 . 这是从以前的标准中明确说明的 . 也许这只是对Std委员会的监督,因为在move
之后没有对迭代者做出类似的澄清,但无论如何都没有这样的保证 .因此,它的长期和短期是你不能假设你的迭代器在
move
之后仍然是好的 .编辑:
n3242草案中的23.2.1 / 11指出:
这可能导致人们得出结论,在
move
之后迭代器是有效的,但我不同意 . 在您的示例代码中,a_iter
是vector
a
中的迭代器 . 在move
之后,那个容器,a
肯定已经改变了 . 我的结论是上述条款不适用于本案 .我认为改变移动构造移动分配的编辑改变了答案 .
至少如果我正确地阅读表格96,移动构造的复杂性为"note B",除了
std::array
之外,这是任何复杂度 . 然而,移动分配的复杂性是线性的 .因此,移动构造基本上没有选择,只能从源复制指针,在这种情况下,很难看到迭代器如何变得无效 .
但是,对于移动分配,线性复杂性意味着它可以选择将单个元素从源移动到目标,在这种情况下,迭代器几乎肯定会变得无效 .
通过描述强化了元素移动分配的可能性:“a的所有现有元素都被移动分配或销毁” . “被破坏”的部分将对应于销毁现有内容,并且从源中“窃取”指针 - 但是“分配给”的移动将指示将各个元素从源移动到目的地 .
由于没有任何东西可以保持迭代器保持对原始容器的引用或指针,我会说你不能依赖迭代器保持有效,除非你在标准中找到明确的保证 .
tl;dr : Yes, moving a std::vector<T, A> possibly invalidates the iterators
常见的情况(使用std :: allocator)是没有发生失效但是没有保证和切换编译器甚至下一个编译器更新可能会使你的代码行为不正确如果你依赖于你的实现当前没有使迭代器无效 .
在移动分配:
移动分配后
std::vector
迭代器是否实际上保持有效的问题与向量模板的分配器感知相关并且取决于分配器类型(以及可能的相应实例) .在我看到的每个实现中,
std::vector<T, std::allocator<T>>
1 的移动赋值实际上不会使迭代器或指针无效 . 然而,当涉及到使用它时,存在一个问题,如 the standard just cannot guarantee that iterators remain valid for any move-assignment of a std::vector instance in general, because the container is allocator aware.自定义分配器可能具有状态,如果它们不在移动分配上传播且不比较相等,则向量必须使用其自己的分配器为移动的元素分配存储 .
让:
现在如果
std::allocator_traits<A>::propagate_on_container_move_assignment::value == false &&
std::allocator_traits<A>::is_always_equal::value == false &&
(可能截至第17页)a.get_allocator() != b.get_allocator()
然后
b
将分配新存储并将a
的元素逐个移动到该存储中,从而使所有迭代器,指针和引用无效 .原因是满足上述条件 1. 禁止在集装箱移动中移动分配器的分配 . 因此,我们必须处理分配器的两个不同实例 . 如果这两个分配器对象现在既不总是比较相等( 2. )也不实际比较相等,则两个分配器都具有不同的状态 . 分配器
x
可能无法释放具有不同状态的另一个分配器y
的内存,因此具有分配器的容器x
不能仅从通过y
分配其内存的容器中窃取内存 .如果分配器在移动分配上传播或两个分配器比较相等,那么实现很可能会选择只生成
a
数据,因为它可以确保能够正确地释放存储 .1 :
std::allocator_traits<std::allocator<T>>::propagate_on_container_move_assignment
和std::allocator_traits<std::allocator<T>>::is_always_equal
都是std::true_type
的typdef(对于任何非专业的std::allocator
) .移动施工:
分配器感知容器的移动构造函数将从当前表达式移动的容器的分配器实例移动构造其分配器实例 . 因此,确保了适当的解除分配能力,并且存储器可以(并且实际上将)被盗,因为移动构造(除了
std::array
)必然具有恒定的复杂性 .注意:即使是移动构造,仍然无法保证迭代器保持有效 .
在交换:
要求两个向量的迭代器在交换后保持有效(现在只是指向相应的交换容器)很容易,因为交换只有定义了行为,如果
std::allocator_traits<A>::propagate_on_container_swap::value == true ||
a.get_allocator() == b.get_allocator()
因此,如果分配器不在swap上传播,并且如果它们不相等,则交换容器首先是未定义的行为 .