51

我知道,通常标准对已移动的值几乎没有要求:

N3485 17.6.5.15 [lib.types.movedfrom]/1:

C++ 标准库中定义的类型的对象可以从 (12.8) 中移出。移动操作可以显式指定或隐式生成。除非另有说明,否则此类移出的对象应置于有效但未指定的状态。

我找不到任何关于vector明确将其从本段中排除的内容。但是,我想不出一个理智的实现来导致向量不为空。

是否有一些我缺少的标准术语,或者这类似于在 C++03 中将其视为basic_string连续缓冲区

4

4 回答 4

74

我来晚了,并提供了一个额外的答案,因为我不相信此时任何其他答案是完全正确的。

问题:

移出向量总是空的吗?

回答:

通常,但不,并非总是如此。

血腥细节:

vector没有像某些类型那样的标准定义的移出状态(例如,在移出后被unique_ptr指定为等于)。nullptr然而,要求vector是这样的,没有太多的选择。

答案取决于我们谈论vector的是移动构造函数还是移动赋值运算符。在后一种情况下,答案还取决于vector' 分配器。


vector<T, A>::vector(vector&& v)

此操作必须具有恒定的复杂性。v这意味着除了从构建中窃取资源之外别无选择*this,并v处于空状态。无论分配器A是什么,无论类型T是什么,这都是正确的。

所以对于移动构造函数,是的,被移动的vector总是空的。这没有直接指定,但不属于复杂性要求,并且没有其他方法可以实现它。


vector<T, A>&
vector<T, A>::operator=(vector&& v)

这要复杂得多。主要有3种情况:

一:

allocator_traits<A>::propagate_on_container_move_assignment::value == true

propagate_on_container_move_assignment评估为true_type

在这种情况下,移动赋值运算符将销毁 中的所有元素*this,使用分配器从 释放容量*this,移动分配分配器,然后将内存缓冲区的所有权从 转移v*this。除了破坏 中的元素外*this,这是一个 O(1) 复杂度操作。并且通常(例如在大多数但不是所有的 std::algorithms 中),移动分配的 lhs 在移动分配empty() == true之前。

注意:在 C++11 中propagate_on_container_move_assignmentforstd::allocatorfalse_type,但这已更改为true_typefor C++1y (我们希望 y == 4)。

在情况一中,搬出者vector将始终为空。

二:

allocator_traits<A>::propagate_on_container_move_assignment::value == false
    && get_allocator() == v.get_allocator()

propagate_on_container_move_assignment计算结果为false_type,并且两个分配器比较相等)

在这种情况下,移动赋值运算符的行为类似于案例一,但有以下例外:

  1. 分配器没有移动分配。
  2. 这个案例和案例三之间的决定发生在运行时,案例三需要更多的T,因此案例二也是如此,即使案例二实际上并没有执行这些额外的要求T

在第二种情况下,搬出者vector将始终为空。

三:

allocator_traits<A>::propagate_on_container_move_assignment::value == false
    && get_allocator() != v.get_allocator()

propagate_on_container_move_assignment计算结果为false_type,并且两个分配器比较不相等)

在这种情况下,实现不能移动分配器,也不能将任何资源从v到转移*this(资源是内存缓冲区)。在这种情况下,实现移动赋值运算符的唯一方法是有效地:

typedef move_iterator<iterator> Ip;
assign(Ip(v.begin()), Ip(v.end()));

也就是说,将每个人T从移动v*thisassign可以重复使用两者capacity,如果可用的话size*this例如,如果*this具有相同sizev实现可以将分配每个T从移动v*this。这T需要MoveAssignable. 请注意,MoveAssignable不需要T具有移动赋值运算符。复制赋值运算符也足够了。 MoveAssignable只是意味着T必须可以从 rvalue 分配T

如果sizeof*this不充分,则T必须在 中构建new *this。这T需要MoveInsertable. 对于我能想到的任何理智的分配器,MoveInsertable归结为与 相同的东西MoveConstructible,这意味着可以从右值构造T(并不意味着存在移动构造函数T)。

在情况三中,搬家vector一般不会是空的。它可能充满了移动的元素。如果元素没有移动构造函数,这可能等同于复制赋值。但是,没有什么可以强制执行此操作。v.clear()如果他愿意,实现者可以自由地做一些额外的工作并执行,v留空。我不知道有任何实现这样做,我也不知道实现这样做的任何动机。但我没有看到任何禁止它的东西。

v.clear()David Rodríguez 报告说,在这种情况下GCC 4.8.1 调用,v留空。 libc++没有,v不留空。两种实现都是一致的。

于 2013-07-18T23:50:57.147 回答
5

虽然在一般情况下它可能不是一个理智的实现,但移动构造函数/赋值的有效实现只是从源复制数据,而不影响源。此外,对于赋值的情况,移动可以实现为交换,并且被移动的容器可能包含被移动到的容器的旧值。

如果您像我们一样使用多态分配器,实际上可以将移动实现为复制,并且分配器不被视为对象的一部分(因此,赋值永远不会改变正在使用的实际分配器)。在这种情况下,移动操作可以检测源和目标是否使用相同的分配器。如果他们使用相同的分配器,则移动操作可以只从源中移动数据。如果他们使用不同的分配器,那么目标必须复制源容器。

于 2013-07-18T18:07:23.563 回答
4

在很多情况下,移动构造和移动分配可以通过委托来实现swap- 特别是在不涉及分配器的情况下。这样做有几个原因:

  • swap无论如何都必须实施
  • 开发人员效率,因为必须编写更少的代码
  • 运行时效率,因为总共执行的操作更少

这是移动分配的示例。在这种情况下,如果移至向量不为空,则移出向量将不为空。

auto operator=(vector&& rhs) -> vector&
{
    if (/* allocator is neither move- nor swap-aware */) {
        swap(rhs);
    } else {
        ...
    }
    return *this;
}
于 2013-07-18T18:07:45.500 回答
0

我在其他答案上留下了对此效果的评论,但在完全解释之前不得不匆忙离开。移出向量的结果必须始终为空,或者在移动赋值的情况下,必须为空或前一个对象的状态(即交换),否则无法满足迭代器无效规则,即移动不会使它们无效。考虑:

std::vector<int> move;
std::vector<int>::iterator it;
{
    std::vector<int> x(some_size);
    it = x.begin();
    move = std::move(x);
}
std::cout << *it;

在这里你可以看到迭代器失效确实暴露了移动的实现。此代码合法的要求,特别是迭代器保持有效,阻止了实现执行复制、小对象存储或任何类似的事情。如果制作了副本,则it在清空可选选项时将失效,如果vector使用某种基于 SSO 的存储也是如此。本质上,唯一合理的可能实现是交换指针,或者简单地移动它们。

请查看所有容器要求的标准报价:

X u(rv)    
X u = rv    

post: u 应等于 rv 在此构造之前的值

a = rv

a 应等于 rv 在此分配之前的值

迭代器有效性是容器值的一部分。虽然标准没有直接明确地说明这一点,但我们可以看到,例如,

begin() 返回一个引用容器中第一个元素的迭代器。end() 返回一个迭代器,它是容器的结束值。如果容器是空的,那么 begin() == end();

任何实际上确实从源元素移动而不是交换内存的实现都是有缺陷的,所以我建议任何标准的措辞否则都是一个缺陷——尤其是因为标准实际上在这一点上并不是很清楚. 这些报价来自 N3691。

于 2013-07-18T18:45:45.920 回答