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


70

如果我在vector中有一个迭代器a,那么我b从中移动向量或移动分配向量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 不会使迭代器无效


1
@chris我希望a_iter现在在移动b之后引用一个元素a
大卫·布朗

6
Pedantic-您没有移动构造,而是移动分配。
John Dibling 2012年

7
@Thomash:如果答案是它确实会使迭代器无效,那么取消引用它们是未定义的行为,那么您将如何测试呢?
本杰明·林德利

1
我想不出迭代器会失效的原因,但是我在标准中找不到任何引号来支持...由于交换后迭代器的有效性是明确定义的,因此认为这是合理的移动时可以应用相同的推理(如果我们考虑如何vectors实现的话甚至更多)。
Luc Touraille 2012年

1
@Luc:如果迭代器类本身维护指向矢量类的指针,则迭代器可能无效。只是吐口水。
John Dibling 2012年

Answers:


25

尽管可以合理地假设iterators在a之后仍然有效move,但我认为标准并不能真正保证这一点。因此,迭代器在之后处于未定义状态move


没有引用我可以在标准发现其中明确规定就存在的之前迭代器move仍然有效之后move

在表面上,这似乎是完全合理的假设,一个iterator通常作为指针到控制的序列实现。如果是这种情况,则迭代器在move

但是的实现iterator是实现定义的。意思是,只要iterator特定平台上的满足标准规定的要求,就可以以任何方式实施。从理论上讲,它可以实现为指向vector类的指针和索引的组合。如果这种情况,则迭代器将在之后变得无效move

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

因此,总而言之,你不能认为在a之后,迭代器仍然是好的move

编辑:

第n3242号草案中的23.2.1 / 11指出:

除非另外指定(显式指定或通过其他函数定义函数),否则调用容器成员函数或将容器作为参数传递给库函数均不得使对该容器内对象的迭代器或更改其值无效。

这可能导致人们得出结论,迭代器在a之后有效move,但我不同意。在您的示例代码中,a_iter是的迭代器vector a。在之后move,该容器a肯定已更改。我的结论是以上条款不适用于这种情况。


1
+1但是,也许你可以合理地假定他们仍然之后一招不错-但我们知道,它可能无法工作,如果你切换编译器。它可以在我刚刚测试过的每个编译器中使用,并且可能永远都可以。
戴维

6
@Dave:对未定义行为的依赖是非常光滑的斜率,并且从技术上讲在技术上无效。您最好不要这样做。
John Dibling 2012年

3
我通常会同意,但是很难编写一个交换来维护有效的迭代器,而移动分配则不这样做。要使迭代器无效,几乎需要图书馆编写者的努力。此外,未定义是我最喜欢的行为。
2012年

@Sven Marnach:我永远不知道要使用天气还是使用天气:)
John Dibling 2012年

6
这是LWG 2321
dyp

12

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

至少如果我正确地读取了表96,则移动构造的复杂性将以“注释B”的形式给出,这对于除以外的任何事物都是恒定的std::array。移动分配的复杂性但是,是线性的。

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

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

元素移动分配的可能性通过以下描述得到了增强:“ a的所有现有元素都已分配或销毁了”。“销毁”部分将对应于销毁现有内容,并从源头“窃取”指针,但“移动到”将指示将各个元素从源头移到目的地。


我在表96中看到了与您相同的东西,但是我震惊的是,移动结构和移动分配具有不同的复杂性要求!符合要求的实现是否必须匹配该表中的复杂性,还是可以做得更好?(又名:是std :: vector,它将指针复制到符合移动分配操作标准的数据中吗?)
David

@戴夫:规范的实施不得比标准中规定的任何性能保证更糟。
John Dibling 2012年

9
我实际上认为这是线性分配的容器的大小。这与线性的析构函数相同,它必须销毁所有现有项,而移动构造函数不存在此问题,因为不存在现有项。
KillianDS 2012年

他们为什么不要求std :: vector移动分配具有恒定的复杂性?(以及所有其他容器...)
David

3
这不仅是要破坏的元素数量呈线性关系。如[container.requirements.general] / 7中所述,移动构造总是移动分配器,如果分配propagate_on_container_move_assignment为true,则移动分配仅移动分配器;如果为true,并且分配器不相等,则现有存储无法移动,因此存在可能的重新分配,并且每个元素都单独移动。
乔纳森·威克利

4

由于没有什么可以阻止迭代器保留对原始容器的引用或指针,因此,除非您在标准中找到明确的保证,否则您不能依靠迭代器保持有效。


+1:我同意这项评估,在过去30分钟内,我一直在寻找这样的参考资料,但我一无所获。:)
John Dibling 2012年

3
同意John Dibling的观点,最接近的参考是,如果两个容器被交换,则迭代器不会无效,这似乎表明它应该是有效的,但是我没有找到任何这种保证。令人惊讶的是,有关集装箱运输的标准如此沉默。
大卫·罗德里格斯(DavidRodríguez)-dribeas 2012年

1
难道不是相反吗?您是否可以假设迭代器保持有效,除非标准另有规定?我从这句话中了解到这一点:Unless otherwise specified (either explicitly or by defining a function in terms of other functions), invoking a container member function or passing a container as an argument to a library function shall not invalidate iterators to, or change the values of, objects within that container.[container.requirements.general]
Luc Touraille 2012年

4
既不vector::swap是恒定时间又没有使迭代器无效,是否会阻止vector::iterator包含指向原始容器的指针?
David Brown

4
@LucTouraille:我不知道。我每个月只能听这么多普通话,而且超出了我的极限。
本杰明·林德利

3

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

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


在移动分配

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

在每一个实现我所看到的,一个的移入分配std::vector<T, std::allocator<T>>1实际上不会无效迭代器或指针。但是存在一个问题,当归结为使用该标准时,因为该标准不能保证迭代器对于a的任何移动分配仍然有效。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的数据,因为它可以确保能够正确地释放存储。

1std::allocator_traits<std::allocator<T>>::propagate_on_container_move_assignmentstd::allocator_traits<std::allocator<T>>::is_always_equal都是typdef的std::true_type(对于任何非专业std::allocator)。


在施工中

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

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

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


交换时

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.