移动的向量是否总是空的?

时间:2013-07-18 17:54:20

标签: c++ c++11 vector language-lawyer move-semantics

我知道通常标准对已经移动的值的要求很少:

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

  

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

我找不到任何关于vector的内容,而是明确地将其排除在本段之外。但是,我无法想出一个理智的实现,导致向量不为空。

是否存在一些我认为缺少或与treating basic_string as a contiguous buffer in C++03类似的标准?

4 个答案:

答案 0 :(得分:54)

我迟到了这个派对,并提供了额外的答案,因为我不相信此时的任何其他答案是完全正确的。

<强>问题:

  

移动的向量是否总是空的?

<强>答案:

通常,但不,不总是。

血腥的细节:

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中的分配器释放容量,移动分配分配器,然后从{{1转移内存缓冲区的所有权到v。除了*this中元素的破坏之外,这是O(1)复杂性操作。通常(例如在大多数但不是所有的std :: algorithms中),移动赋值的lhs在移动赋值之前都有*this

注意:在C ++ 11中,empty() == true的{​​{1}}为propagate_on_container_move_assignment,但对于C ++ 1y,这已更改为std::allocator(y == 4我们希望)。

如果是一,移动的false_type将始终为空。

两个

true_type

vector评估为allocator_traits<A>::propagate_on_container_move_assignment::value == false && get_allocator() == v.get_allocator() ,两个分配器比较相等)

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

  1. 分配器未分配。
  2. 本案例和案例3之间的决定发生在运行时,案例3需要更多propagate_on_container_move_assignment,因此情况二也是如此,即使案例2实际上没有对{{{{{{ 1}}。
  3. 在案例二中,移动的false_type将始终为空。

    三:

    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。例如,如果capacitysize具有相同的*this,则实施可以将每个*thissize分配到v。这需要Tv。请注意,*this不需要T来设置移动分配运算符。复制赋值运算符也足够了。 MoveAssignable只是意味着MoveAssignable必须可以从右值T分配。

    如果MoveAssignable的{​​{1}}不够,则必须在T中构建新的T。这需要size*this。对于我能想到的任何理智的分配器,T归结为与*this相同的东西,这意味着可以从右值T构造(并不意味着{for}的移动构造函数的存在{1}})。

    在案例三中,移动的MoveInsertable通常不会为空。它可能充满了移动元素。如果元素没有移动构造函数,则这可能等同于复制赋值。但是,没有任何要求这样做。如果他愿意,执行者可以自由地做一些额外的工作并执行MoveInsertable,让MoveConstructible为空。我不知道有任何实施这样做,我也没有意识到实施的任何动机。但我没有看到任何禁止它的东西。

    DavidRodríguez报告说,在这种情况下,GCC 4.8.1调用T,使T为空。 libc++没有,vector不为空。两种实现都是一致的。

答案 1 :(得分:5)

虽然在一般情况下它可能不是 sane 实现,但移动构造函数/赋值的有效实现只是从源复制数据,保持源不变。此外,对于赋值的情况,move可以实现为swap, move-from 容器可能包含 move-to 容器的旧值。

如果你像我们一样使用多态分配器,实际上可以实现move作为副本,并且分配器不被认为是对象的的一部分(因此,赋值永远不会改变正在使用的实际分配器)。在此上下文中,移动操作可以检测源和目标是否使用相同的分配器。如果它们使用相同的分配器,则移动操作只能从源移动数据。如果它们使用不同的分配器,则目标必须复制源容器。

答案 2 :(得分:3)

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

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

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

auto operator=(vector&& rhs) -> vector&
{
    if (/* allocator is neither move- nor swap-aware */) {
        swap(rhs);
    } else {
        ...
    }
    return *this;
}

答案 3 :(得分: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    
  

帖子:你应该等于rv在这个结构之前的价值

a = rv
  

a应等于rv在此赋值之前的值

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

  

begin()返回一个迭代器,引用该中的第一个元素   容器。 end()返回一个迭代器,它是一个过去的结束值   对于容器。如果容器为空,则begin()== end();

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