【问题标题】:Does moving a vector invalidate iterators?移动向量是否会使迭代器无效?
【发布时间】:2012-06-16 19:36:44
【问题描述】:

如果我有一个指向向量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::swapdoes not invalidate iterators

【问题讨论】:

  • @chris 我希望a_iter 现在在移动a 后引用b 中的元素。
  • 学究式——你没有移动构造,你移动分配。
  • @Thomash:如果答案是它确实使迭代器无效,那么取消引用它们是未定义的行为,那么你将如何测试它?
  • 我想不出迭代器无效的原因,但我在标准中找不到任何引用来支持这一点......因为交换后迭代器的有效性很好 -定义,认为在移动时可以应用相同的推理似乎是合理的(如果我们考虑如何实现vectors,则更是如此)。
  • @Luc:如果迭代器类本身维护指向向量类的指针,则迭代器可能会失效。只是随地吐痰。

标签: c++ iterator c++11


【解决方案1】:

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

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


移动任务中

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

在我见过的每个实现中,std::vector&lt;T, std::allocator&lt;T&gt;&gt;1 的移动赋值实际上不会使迭代器或指针无效。然而,当涉及到使用它时存在一个问题,因为标准不能保证迭代器对于std::vector 实例的任何移动分配通常保持有效,因为容器是分配器感知的。

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

让:

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

现在如果

  1. std::allocator_traits&lt;A&gt;::propagate_on_container_move_assignment::value == false &amp;&amp;
  2. std::allocator_traits&lt;A&gt;::is_always_equal::value == false &amp;&amp;可能从 c++17 开始
  3. a.get_allocator() != b.get_allocator()

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

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

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

1std::allocator_traits&lt;std::allocator&lt;T&gt;&gt;::propagate_on_container_move_assignmentstd::allocator_traits&lt;std::allocator&lt;T&gt;&gt;::is_always_equal 都是 std::true_type 的 typdefs(对于任何非专业的 std::allocator)。


施工中

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

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

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


交换时

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

  1. std::allocator_traits&lt;A&gt;::propagate_on_container_swap::value == true ||
  2. a.get_allocator() == b.get_allocator()

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

【讨论】:

    【解决方案2】:

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


    我在标准中找不到任何参考明确指出move 之前存在的迭代器在move 之后仍然有效。 p>

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

    但是iterator 的实现是实现定义的。这意味着,只要特定平台上的iterator 符合标准规定的要求,它就可以以任何方式实施。从理论上讲,它可以实现为指向vector 类的指针与索引的组合。如果,那么在move之后迭代器就会失效。

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

    因此,总而言之,您不能假设您的迭代器在 move 之后仍然很好。

    编辑:

    n3242 草案中的 23.2.1/11 指出:

    除非另有说明(明确或通过定义 其他功能的功能),调用容器成员 函数或将容器作为参数传递给库函数 不应使对象的迭代器无效或更改其值 在那个容器内。

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

    【讨论】:

    • +1 但是,也许您可​​以合理地假设它们在移动后仍然 - 但只要知道如果您切换编译器它可能不起作用。它适用于我刚刚测试过的每一个编译器,而且可能永远都会。
    • @Dave:对未定义行为的依赖是一个非常危险的斜坡,从技术上讲,它是无效的。你最好不要这样做。
    • 我通常会同意,但是很难编写一个维护有效迭代器的交换和一个不维护有效迭代器的移动分配。库编写者几乎需要刻意努力才能使迭代器失效。此外,未定义是我最喜欢的一种行为。
    • @Sven Marnach:我从来不知道 Weather 要使用 Wea​​ther 还是是否 :)
    • 这是LWG 2321
    【解决方案3】:

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

    【讨论】:

    • +1:我同意这个评估,而且我在过去 30 分钟里一直在寻找这样的参考,但我找不到任何东西。 :)
    • 同意John Dibling,最接近的参考是如果交换两个容器,迭代器不会失效,这似乎表明它应该是有效的,但我没有找到任何这样的保证。令人惊讶的是,关于容器移动的标准如此沉默。
    • 不是反过来吗?除非标准另有规定,否则您不能假设迭代器仍然有效吗?这就是我从这句话中所理解的: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]
    • vector::swap 是常数时间并且不会使迭代器无效阻止 vector::iterator 包含指向原始容器的指针吗?
    • @LucTouraille:我不知道。我每个月只能理解这么多标准语,已经超出了我的极限。
    【解决方案4】:

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

    至少如果我正确地阅读了表 96,则移动构造的复杂性被给出为“注释 B”,对于除std::array 之外的任何东西都是恒定的复杂性。然而,移动赋值的复杂度是线性的。

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

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

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

    【讨论】:

    • 我在表 96 中看到了和你一样的东西,但我很震惊移动构造和移动分配有不同的复杂度要求!符合标准的实现是否具有 以匹配该表中的复杂性,还是可以做得更好? (AKA:是一个 std::vector 复制指向它的数据的指针符合移动赋值操作标准吗?)
    • @Dave:符合标准的实现不得比标准中规定的任何性能保证差。
    • 我实际上认为它在分配给容器的大小方面是线性的。这与析构函数是线性的相同,它必须销毁所有现有项目,移动构造函数没有这个问题,因为没有现有项目。
    • 为什么他们不需要 std::vector 移动分配的恒定复杂性?! (以及所有其他容器......)
    • 要销毁的元素数量不仅仅是线性的。如 [container.requirements.general]/7 中所述,移动构造总是移动分配器,移动分配仅在 propagate_on_container_move_assignment 为真时移动分配器,如果不为真且分配器不相等,则无法移动现有存储,并且因此可能会重新分配,并且每个元素都会单独移动。
    猜你喜欢
    • 2011-04-22
    • 2010-12-10
    • 1970-01-01
    • 1970-01-01
    • 2012-08-09
    • 2011-05-06
    • 1970-01-01
    • 2015-07-31
    相关资源
    最近更新 更多