移动向量会使迭代器失效吗?

如果我有一个到向量 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 不会使迭代器无效

5664 次浏览

我认为修改了移动构造以移动作业的编辑改变了答案。

至少如果我正确地阅读了表96,move 构造的复杂度被给出为“ Note B”,这是除 std::array以外的任何东西的常量复杂度。然而,移动 任务的复杂性是线性的。

因此,move 构造基本上别无选择,只能从源代码复制指针,在这种情况下,很难看出迭代器如何变得无效。

然而,对于移动分配,线性复杂性意味着 可以选择将单个元素从源移动到目的地,在这种情况下迭代器几乎肯定会变得无效。

元素移动分配的可能性通过描述得到加强: “一个元素的所有现有元素要么被移动分配,要么被销毁”。“销毁”部分相当于销毁现有的内容,并“窃取”源中的指针——但是“移动指定”将表示将单个元素从源移动到目的地。

因为没有什么可以阻止迭代器保持对原始容器的引用或指针,所以我要说,除非在标准中找到明确的保证,否则不能依赖于仍然有效的迭代器。

虽然可以合理地假设 iteratormove之后仍然有效,但我不认为标准实际上保证了这一点。因此,迭代器在 move之后处于未定义状态。


没有任何参考,我可以找到在 明确指出的标准,即迭代器存在之前的 move仍然有效的 之后move

从表面上看,假设 iterator是作为指向受控序列的指针实现的 一般来说似乎是完全合理的。如果是这种情况,那么迭代器在 move之后仍然有效。

但是 iterator的实现是实现定义的。也就是说,只要在特定平台上的 iterator满足标准规定的要求,它就可以以任何方式实现。理论上,它可以作为返回到 vector类的指针和索引的组合来实现。如果是 那是,那么在 move之后迭代器将变得无效。

iterator是否以这种方式实现是无关紧要的。它可以以这种方式实现,因此如果没有来自标准的后 move迭代器仍然有效的特定保证,您就不能假定它们是有效的。还要记住,在 swap之后的迭代器有 这样的保证。这是从以前的标准中明确澄清的。也许这只是 Std 委员会的疏忽,没有在 move之后对迭代器进行类似的澄清,但无论如何都没有这样的保证。

因此,长话短说就是你不能假设你的迭代器在 move之后仍然是好的。

编辑:

23.2.1/11在第3242号草案中指出:

除非另有说明(显式地或通过定义 函数) ,调用容器成员 函数或将容器作为参数传递给库函数 不会使迭代器失效或更改对象的值 在那个集装箱里。

这可能会导致人们得出结论,迭代器在 move之后是有效的,但是我不同意。在您的示例代码中,a_itervector a中的迭代器。在 move之后,那个容器,a肯定已经改变了。我的结论是上述条款不适用于本案。

Dr: 是的,移动 std::vector<T, A>可能会使迭代器失效

通常情况下(在 std::allocator就位的情况下) ,无效不会发生,但是没有保证,切换编译器或者甚至下一次编译器更新可能会使代码行为不正确,如果你依赖于你的实现当前没有使迭代器无效的事实。


移动任务 :

在移动分配之后,std::vector迭代器是否能够保持有效的问题与向量模板的分配器感知相关,并且取决于分配器类型(可能还有它们各自的实例)。

在我看到的每一个实现中,一个 std::vector<T, std::allocator<T>>< sup > 1 的 move ——赋值实际上并不会使迭代器或指针失效。然而,当涉及到如何使用 标准只是不能保证迭代器在一般情况下对于 std::vector实例的任何移动分配都是有效的,因为容器是分配器感知的。时,存在一个问题

自定义分配器可能具有状态,如果它们在移动分配时没有传播,并且不进行相等的比较,则向量必须使用自己的分配器为移动的元素分配存储空间。

让我们:

std::vector<T, A> a{/*...*/};
std::vector<T, A> b;
b = std::move(a);

如果

  1. std::allocator_traits<A>::propagate_on_container_move_assignment::value == false &&
  2. std::allocator_traits<A>::is_always_equal::value == false &&(可能是 c + + 17)
  3. a.get_allocator() != b.get_allocator()

然后 b将分配新的存储并将 a的元素一个一个地移动到该存储中,从而使所有迭代器、指针和引用失效。

原因是以上条件的实现 1.禁止分配器在集装箱移动时进行移动分配。因此,我们必须处理分配器的两个不同实例。如果这两个分配器对象现在既不总是比较相等(2.) ,也不实际比较相等,那么两个分配器都有不同的状态。分配器 x可能无法释放具有不同状态的另一个分配器 y的内存,因此具有分配器 x的容器不能仅仅从通过 y分配其内存的容器中窃取内存。

如果分配器在移动分配时传播,或者两个分配器比较相等,那么实现很可能选择让 b拥有 a数据,因为它可以确保能够正确释放存储。

1 : std::allocator_traits<std::allocator<T>>::propagate_on_container_move_assignmentstd::allocator_traits<std::allocator<T>>::is_always_equal都是 std::true_type的类型(对于任何非专门化的 std::allocator)。


移动建设 :

std::vector<T, A> a{/*...*/};
std::vector<T, A> b(std::move(a));

支持分配器的容器的 move 构造函数将移动——从当前表达式移动的容器的分配器实例构造它的分配器实例。因此,正确的释放能力得到了保证,内存可以(事实上也将会)被窃取,因为 move 构造(除了 std::array之外)必须具有恒定的复杂性。

注意: 即使对于 move 构造,迭代器仍然不能保证有效。


关于互换:

要求两个向量的迭代器在交换之后保持有效(现在只是指向相应的交换容器)是很容易的,因为交换只有在

  1. std::allocator_traits<A>::propagate_on_container_swap::value == true ||
  2. a.get_allocator() == b.get_allocator()

因此,如果分配器不在交换上传播,并且它们之间的比较不相等,那么交换容器首先就是未定义的行为。