我有一个类,其中包含一个容器,以及一个容器中的迭代器。如何正确实现移动构造函数?我似乎记得,通过标准,你不能依赖于移动后剩余有效的迭代器(这是如此愚蠢)。是否有一些方法可以“更新”迭代器,如果它是无效的或什么?或者我是否必须动态分配容器,移动它,然后使迭代器保持有效?
答案 0 :(得分:3)
更新:使用std::unique_ptr
作为容器的持有者是规范的通用解决方案 - 只需移动容器,只需转移所有权并交换迭代器即可。正如您已经说过的那样,您可以将此作为优化,但我希望通用解决方案也非常高效,并且在证明它是真实的之后我只接受代码的更多复杂性(也就是bug潜力)。为您的用例赢得性能。
我将把以下的答案留给未来的读者:阅读它和评论,看看为什么其他解决方案没有真正起作用,以及在哪些情况下它们会造成麻烦。
更新迭代器的显而易见的方法是:
Container c = ...;
Container::iterator it = ...;
const auto d = std::distance( c.begin(), it );
Container n = std::move(c);
it = n.begin();
std::advance( it, d );
通常是线性的,但当迭代器是随机访问迭代器时是常量。
由于您可能不想这样做,因此您有两个选项可以提供帮助:默认构造新容器并使用swap
而不使迭代器失效或将容器放入std::unique_ptr
并改为移动它。
第一种方法(swap
)要求两个实例都有容器实例,这可能比存储在std::unique_ptr
中的简单单指针稍大一些。当您经常移动实例时,基于std::unique_ptr
的方法似乎比我更好,尽管每次访问都需要一个指针间接。判断(并衡量)适合自己的最佳选择。
答案 1 :(得分:1)
我认为迭代器失效的隐含保证适用于移动ctor。也就是说,以下内容适用于所有容器,但std::array
:
template<class Container>
struct foo_base
{
Container c;
Container::iterator i;
foo_base(foo_base&& rhs, bool is_end)
: c( std::move(rhs.c) )
, i( get_init(is_end, rhs.i) )
{}
Container::iterator get_init(bool is_end, Container::iterator ri)
{
using std::end; // enable ADL
return is_end ? end(c) : ri;
}
};
template<class Container>
struct foo : private foo_base<Container>
{
foo(foo&& rhs)
: foo_base(std::move(rhs), rhs.i == end(rhs.c))
{}
};
通过基类进行复杂的初始化是必要的,因为如果分配器没有为移动分配传播,则不需要移动移动。需要检查迭代器,因为end()
迭代器可能无效;必须在移动容器之前执行此检查。但是,如果您可以确保分配器传播(或者移动分配不会使您的情况的迭代器无效),则可以使用下面的更简单版本,将swap
替换为移动分配。
N.B。 get_init
功能的唯一目的是启用ADL。 foo_base
可能有成员函数end
,这将禁用ADL。 using-declaration 停止非限定查找以查找可能的成员函数,因此始终执行ADL。你可以使用std::end(c)
并摆脱get_init
,如果你对在这里输掉ADL感到满意。
如果事实证明移动ctor没有这种隐含的保证,那么swap
仍有明确的保证。为此,您可以使用:
template<class Container>
struct foo
{
Container c;
Container::iterator i;
foo(foo&& rhs)
{
using std::end; // enable ADL
bool const is_end = (rhs.i == end(rhs.c));
c.swap( rhs.c );
i = get_init(is_end, rhs.i);
}
Container::iterator get_init(bool is_end, Container::iterator ri)
{
using std::end; // enable ADL
return is_end ? end(c) : ri;
}
};
但是,交换有一些要求,在[container.requirements.general] / 7 + 8中定义:
调用容器的
swap
函数的行为是未定义的,除非被交换的对象具有比较相等的分配器或allocator_traits<allocator_type>::propagate_on_container_swap::value
为true
[...] 属于Compare
和Pred
的任何Hash
,a
或b
个对象都应该是可交换的,并且应通过对非成员{{1 }}。如果swap
为allocator_traits<allocator_type>::propagate_on_container_swap::value
,则true
和a
的分配器也应使用对非成员b
的无限制调用进行交换。否则,它们不应被交换,并且行为是未定义的,除非swap
。
即。两个容器都应该(但不一定)具有相同的分配器。
移动构造OTOH只要求不抛出任何异常(对于分配器感知容器);总是移动分配器。