72

如果我有一个指向 vector 的迭代器a,那么我 move-construct 或 move-assign vector bfrom a,该迭代器是否仍然指向同一个元素(现在在 vector 中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 不会使迭代器无效

4

4 回答 4

26

虽然假设iterators 在 a 之后仍然有效可能是合理的move,但我认为标准实际上并不能保证这一点。因此,迭代器在move.


我在标准中找不到任何参考,该参考明确指出在a 之前存在的迭代movemove.

iterator从表面上看,假设 an通常实现指向受控序列的指针似乎是完全合理的。如果是这种情况,那么迭代器在move.

但是 an 的iterator实现是实现定义的。这意味着,只要iterator在特定平台上满足标准规定的要求,它就可以以任何方式实施。从理论上讲,它可以实现为返回vector类的指针和索引的组合。如果这种情况,那么迭代器将在move.

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

因此,总而言之,你不能假设你的迭代器在move.

编辑:

草案 n3242 中的 23.2.1/11 规定:

除非另有规定(明确地或通过根据其他函数定义函数),调用容器成员函数或将容器作为参数传递给库函数不应使该容器内对象的迭代器无效或更改其值.

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

于 2012-06-13T20:00:37.920 回答
12

我认为将移动构造更改为移动分配的编辑会改变答案。

至少如果我正确地阅读了表 96,移动构造的复杂性被给出为“注释 B”,除了std::array. 然而,移动分配的复杂度是线性的。

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

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

描述加强了元素移动分配的可能性:“a 的所有现有元素要么被移动分配,要么被销毁”。“销毁”部分对应于销毁现有内容,并从源“窃取”指针——但“移动分配到”表示将单个元素从源移动到目标。

于 2012-06-13T19:36:50.427 回答
5

tl;dr :是的,移动 astd::vector<T, A>可能会使迭代器无效

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


移动任务

在移动赋值之后迭代器是否std::vector实际上保持有效的问题与向量模板的分配器意识有关,并且取决于分配器类型(可能还有其各自的实例)。

在我见过的每个实现中,移动赋值std::vector<T, std::allocator<T>>1实际上不会使迭代器或指针无效。然而,当涉及到使用它时存在一个问题,因为标准不能保证迭代器对于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_assignment并且std::allocator_traits<std::allocator<T>>::is_always_equal两者都是std::true_type(对于任何非专业的std::allocator)的 typdefs。


移动施工

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

分配器感知容器的移动构造函数将从当前表达式从中移动的容器的分配器实例中移动构造其分配器实例。因此,确保了适当的释放能力,并且内存可以(实际上将)被盗,因为移动构造(除了std::array)必然具有恒定的复杂性。

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


交换时

要求两个向量的迭代器在交换后保持有效(现在只是指向相应的交换容器)很容易,因为交换只有在以下情况下才具有定义的行为

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

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

于 2016-11-06T03:05:36.353 回答
4

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

于 2012-06-13T19:49:45.157 回答