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

Dav*_*own 66 c++ iterator c++11

如果我有一个迭代到载体a,然后我布展构建体或移动指派矢量ba,是否仍迭代器指向同一元件(现在矢量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;
}
Run Code Online (Sandbox Code Playgroud)

是否a_iter仍然有效的,因为a已移入b,或者是通过招无效的迭代器?作为参考,std::vector::swap 不会使迭代器无效.

Joh*_*ing 24

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


没有引用我可以在其中找到标准明确规定,以前一个已经存在的迭代器move仍然有效之后move.

在表面上,这似乎是完全合理的假设,一个iterator通常作为指针到控制的序列实现.如果是这种情况,那么迭代器在之后仍然有效move.

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

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

因此,它的长期和短期是你不能假设你的迭代器仍然很好move.

编辑:

n3242草案中的23.2.1/11指出:

除非另有规定(显式或通过在其它功能来定义的函数),调用一个容器成员函数或传递容器作为参数传递给一个库功能必须迭代不失效,或者改变的值,即容器内的对象.

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

  • @Dave:对未定义行为的依赖是一个非常滑坡,并且在技术上无效.你最好不要这样做. (6认同)
  • 这是[LWG 2321](http://www.open-std.org/JTC1/SC22/WG21/docs/lwg-active.html#2321) (5认同)
  • 我通常会同意,但是很难编写一个维护有效迭代器的交换和一个不支持的移动赋值.库编写者几乎需要有意识地使迭代器失效.此外,undefined是我最喜欢的行为. (2认同)

Jer*_*fin 11

我认为改变移动构造移动分配的编辑改变了答案.

至少如果我正确地阅读表格96,移动构造的复杂性被给出为"音符B",这对于除了之外的任何事物都是恒定的复杂性std::array.然而,移动分配的复杂性是线性的.

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

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

通过描述强化了元素移动分配的可能性:"a的所有现有元素都被移动分配或销毁"."被破坏"的部分将对应于销毁现有内容,并且从源中"窃取"指针 - 但是"分配给"的移动将指示将各个元素从源移动到目的地.

  • 实际上我认为它在分配给容器的大小方面是线性的.这与析构函数是线性的相同,它必须销毁所有现有项,这是move-constructor没有的问题,因为没有现有项. (8认同)
  • 它不仅仅是要销毁的元素数量的线性.如[container.requirements.general]/7中所述,移动构造总是移动分配器,移动分配仅在`propagate_on_container_move_assignment`为真时移动分配器,如果不是真且分配器不相等则则不能移动现有存储器所以可能会重新分配,每个元素都会单独移动. (2认同)

Pix*_*ist 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);
Run Code Online (Sandbox Code Playgroud)

现在如果

  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()

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

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

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

1std::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));
Run Code Online (Sandbox Code Playgroud)

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

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


交换时

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

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

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