如果我理解正确,std::vector::insert
不保证std::vector
的提交或回滚(由于显而易见的原因,它会对std::list
执行),如果抛出异常在复制或移动期间,因为检查异常的成本很高。我最近看到push_back
确保在最后成功插入或没有任何反应。
我的问题如下。假设在push_pack
期间,必须调整向量的大小(重新分配发生)。在这种情况下,必须通过复制或移动语义将所有元素复制到新容器中。假设不保证移动构造函数noexcept
,std::vector
将使用复制语义。因此,为了保证push_pack
的上述行为,std::vector
必须检查是否成功复制,如果没有,则通过交换回滚初始向量。这是发生了什么,如果是这样,这不是很昂贵吗?或者,由于重新分配很少发生,可以说摊销成本很低?
答案 0 :(得分:21)
在C ++ 98/03中,我们(显然)没有移动语义,只有复制语义。在C ++ 98/03中,push_back
具有很强的保证。 C ++ 11中强烈的动机之一就是不要破坏依赖这种强有力保证的现有代码。
C ++ 11规则是:
is_nothrow_move_constructible<T>::value
为真,请移动,否则is_copy_constructible<T>::value
为真,请复制,否则is_move_constructible<T>::value
为真,请移动,否则如果我们是1或2,我们有strong guarantee。如果我们在案例3中,我们只有basic guarantee。因为在C ++ 98/03中,我们总是在案例2中,我们具有向后兼容性。
案例2保持强有力的保证并不昂贵。一个分配新缓冲区,优选地使用诸如第二矢量的RAII设备。复制到其中,并且只有在所有成功完成后,才将*this
与RAII设备交换。这是最便宜的做事方式,无论你是否想要强有力的保证,你都可以免费获得。
案例1保持强有力的保证也很便宜。我所知道的最好的方法是首先将新元素复制/移动到新分配的中间。如果成功,则从旧缓冲区移动元素并交换。
比你想要的更多细节
libc++使用相同的算法完成所有3个案例。为此,使用了两个工具:
std::move_if_noexcept
vector
- 类似容器,其中数据是连续的,但可以从分配的缓冲区的开头处以非零偏移量开始。 libc ++调用这个东西split_buffer
。假设重新分配的情况(非重新分配的情况很简单),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_buffer
。 move_if_noexcept
用于此目的,其返回类型为T const&
或T&&
,完全符合上述3种情况所指定的要求。在每次成功移动/复制时,split_buffer
正在执行push_front
。如果成功,split_buffer
现在具有size() == this->size()+1
,并且其第一个元素与其分配的缓冲区的开头处于零偏移量。如果任何移动/复制失败,split_buffer
的析构函数会破坏split_buffer
中的任何内容并释放缓冲区。
接下来split_buffer
和this
交换数据缓冲区。这是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
来减少重新分配对性能的影响(这需要知道元素计数的估算值)