std :: vector :: push_back的成本是成功还是没有效果?

时间:2014-05-25 19:23:21

标签: c++ c++11 vector stl

如果我理解正确,std::vector::insert不保证std::vector的提交或回滚(由于显而易见的原因,它会对std::list执行),如果抛出异常在复制或移动期间,因为检查异常的成本很高。我最近看到push_back确保在最后成功插入或没有任何反应。

我的问题如下。假设在push_pack期间,必须调整向量的大小(重新分配发生)。在这种情况下,必须通过复制或移动语义将所有元素复制到新容器中。假设不保证移动构造函数noexceptstd::vector将使用复制语义。因此,为了保证push_pack的上述行为,std::vector必须检查是否成功复制,如果没有,则通过交换回滚初始向量。这是发生了什么,如果是这样,这不是很昂贵吗?或者,由于重新分配很少发生,可以说摊销成本很低?

3 个答案:

答案 0 :(得分:21)

在C ++ 98/03中,我们(显然)没有移动语义,只有复制语义。在C ++ 98/03中,push_back具有很强的保证。 C ++ 11中强烈的动机之一就是不要破坏依赖这种强有力保证的现有代码。

C ++ 11规则是:

  1. 如果is_nothrow_move_constructible<T>::value为真,请移动,否则
  2. 如果is_copy_constructible<T>::value为真,请复制,否则
  3. 如果is_move_constructible<T>::value为真,请移动,否则
  4. 代码格式不正确。
  5. 如果我们是1或2,我们有strong guarantee。如果我们在案例3中,我们只有basic guarantee。因为在C ++ 98/03中,我们总是在案例2中,我们具有向后兼容性。

    案例2保持强有力的保证并不昂贵。一个分配新缓冲区,优选地使用诸如第二矢量的RAII设备。复制到其中,并且只有在所有成功完成后,才将*this与RAII设备交换。这是最便宜的做事方式,无论你是否想要强有力的保证,你都可以免费获得。

    案例1保持强有力的保证也很便宜。我所知道的最好的方法是首先将新元素复制/移动到新分配的中间。如果成功,则从旧缓冲区移动元素并交换。

    比你想要的更多细节

    libc++使用相同的算法完成所有3个案例。为此,使用了两个工具:

    1. std::move_if_noexcept
    2. 非std vector - 类似容器,其中数据是连续的,但可以从分配的缓冲区的开头处以非零偏移量开始。 libc ++调用这个东西split_buffer
    3. 假设重新分配的情况(非重新分配的情况很简单),split_buffer构造时引用了这个vector的分配器,其容量是vector的两倍。 },并将其起始位置设为this->size()(尽管split_buffer仍为empty())。

      然后将新元素复制或移动(取决于我们所讨论的push_back重载)到split_buffer。如果失败,split_buffer析构函数将取消分配。如果成功,则split_buffer现在具有size() == 1,并且split_buffer在第一个元素之前具有确切this->size()个元素的空间。

      接下来,元素以相反的顺序this移动/复制到split_buffermove_if_noexcept用于此目的,其返回类型为T const&T&&,完全符合上述3种情况所指定的要求。在每次成功移动/复制时,split_buffer正在执行push_front。如果成功,split_buffer现在具有size() == this->size()+1,并且其第一个元素与其分配的缓冲区的开头处于零偏移量。如果任何移动/复制失败,split_buffer的析构函数会破坏split_buffer中的任何内容并释放缓冲区。

      接下来split_bufferthis交换数据缓冲区。这是noexcept操作。

      最后,split_buffer破坏,破坏其所有元素,并释放其数据缓冲区。

      不需要尝试捕获。没有额外的费用。一切都按照C ++ 11的规定运行(并在上面进行了总结)。

答案 1 :(得分:3)

这就是正在发生的事情,它可能很昂贵。它是decided,push_back的强大保证比移动语义的性能更重要。

答案 2 :(得分:3)

重新分配是昂贵的,是的。这就是为什么重新分配的成本会在std::vector::push_back的多次调用中摊销的原因。

最常见的是,通过将旧尺寸乘以某个常数因子(例如1.5或2)来分配新的尺寸块来完成 - 如1,2,4,8,16,32,......

重新分配时,std::vector分配新块,复制元素,销毁旧元素并释放旧块。如果失败(抛出异常),我们可以简单地中止复制过程并开始销毁复制的元素 - 因此最坏情况下的成本是2*(n-1)+d,其中d是释放的成本(我们复制n-1元素,当复制第n个元素时,抛出异常,因此我们销毁n-1个元素,并释放新块)

注意,当移动不是noexcept时,上述情况适用。如果移动是noexcept,则唯一可能的失败点是分配新的内存块(因为析构函数必须是noexcept),并且重新分配过程更简单,更快。

您可以通过使用不同的容器(如问题不存在的std::deque)或事先使用std::vector::reserve来减少重新分配对性能的影响(这需要知道元素计数的估算值)