如果我有一个向量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
does not invalidate iterators。
答案 0 :(得分:23)
虽然可以合理地假设iterator
在move
之后仍然有效,但我认为标准不能保证这一点。因此,迭代器在move
之后处于未定义状态。
我在标准中找不到任何引用明确指出 move
之前存在的迭代器 {{1}之后}。
从表面上看,假设move
通常通过实现为受控序列的指针似乎是完全合理的。如果是这种情况,那么迭代器在iterator
之后仍然有效。
但是move
的实现是实现定义的。意思是,只要特定平台上的iterator
符合标准规定的要求,就可以以任何方式实施。理论上,它可以实现为指向iterator
类的指针和索引的组合。如果那是的情况,那么迭代器将在vector
之后变为无效。
move
是否实际以这种方式实现是无关紧要的。它可以通过这种方式实现,因此如果没有标准的特定保证,那么post iterator
迭代器仍然有效,你就不能认为它们是有效的。还要记住是在move
之后对迭代器的这种保证。这是从以前的标准中明确说明的。也许这只是对Std委员会的监督,即在swap
之后没有对迭代器做出类似的澄清,但无论如何都没有这样的保证。
因此,长期和短期是你不能假设你的迭代器在move
之后仍然很好。
除非另有说明(明确或通过定义 函数就其他函数而言),调用一个容器成员 函数或将容器作为参数传递给库函数 不得使迭代器无效或更改对象的值 在那个容器内。
这可能导致人们得出结论:move
之后迭代器是有效的,但我不同意。在您的示例代码中,move
是a_iter
vector
中的迭代器。在a
之后,该容器move
肯定已被更改。我的结论是上述条款不适用于这种情况。
答案 1 :(得分:10)
我认为更改移动构造移动分配的编辑会改变答案。
至少如果我正确地阅读表格96,移动构造的复杂性被给出为“音符B”,这对于除std::array
之外的任何事物都是恒定的复杂度。但是,移动赋值的复杂性是线性的。
因此,移动构造基本上没有选择,只能从源复制指针,在这种情况下,很难看到迭代器如何变得无效。
对于移动分配,线性复杂性意味着可以选择将单个元素从源移动到目标,在这种情况下,迭代器几乎肯定会变为无效。
通过描述强化了元素移动分配的可能性:“a的所有现有元素都被移动分配或销毁”。 “被破坏”的部分将对应于销毁现有内容,并从源中“窃取”指针 - 但“分配给”的移动将指示将各个元素从源移动到目的地。
答案 2 :(得分:3)
由于没有任何东西可以保持迭代器保持对原始容器的引用或指针,我会说你不能依赖迭代器保持有效,除非你在标准中找到明确的保证。
答案 3 :(得分:1)
tl; dr:是的,移动std::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);
现在如果
std::allocator_traits<A>::propagate_on_container_move_assignment::value == false &&
std::allocator_traits<A>::is_always_equal::value == false &&
(可能从c ++ 17开始)a.get_allocator() != b.get_allocator()
然后b
将分配新存储并将a
的元素逐个移动到该存储中,从而使所有迭代器,指针和引用无效。
原因是满足上述条件 1。禁止在集装箱移动中移动分配器的分配。因此,我们必须处理分配器的两个不同实例。如果这两个分配器对象现在既不总是比较相等( 2。)也不实际比较相等,则两个分配器都具有不同的状态。分配器x
可能无法释放具有不同状态的另一个分配器y
的内存,因此具有分配器x
的容器不能仅从通过{分配其内存的容器中窃取内存{1}}。
如果分配器在移动分配上传播或两个分配器比较相等,那么实现很可能会选择只生成y
个b
个数据,因为它可以确保能够解除分配存储得当。
1 :a
和std::allocator_traits<std::allocator<T>>::propagate_on_container_move_assignment
都是std::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));
)必然具有复杂性。
注意:即使是移动构造,仍然无法保证迭代器保持有效。
在交换:
要求两个向量的迭代器在交换后保持有效(现在只是指向相应的交换容器)很容易,因为交换只有定义了行为
std::array
std::allocator_traits<A>::propagate_on_container_swap::value == true ||
因此,如果分配器不在swap上传播,并且如果它们不相等,则交换容器首先是未定义的行为。