与 std::inserter 相比,std::back_inserter 有啥好处?
Posted
技术标签:
【中文标题】与 std::inserter 相比,std::back_inserter 有啥好处?【英文标题】:What's the benefit of std::back_inserter over std::inserter?与 std::inserter 相比,std::back_inserter 有什么好处? 【发布时间】:2014-12-06 02:30:51 【问题描述】:据我所知,std::back_inserter
在 STL 算法中工作的任何地方,您都可以传递一个由 .end()
构造的 std::inserter
:
std::copy(l.begin(), l.end(), std::back_inserter(dest_list));
std::copy(l.begin(), l.end(), std::inserter(dest_list, dest_list.end()));
并且,与back_inserter
不同,据我所知inserter
适用于任何 STL 容器!我为std::vector
、std::list
、std::map
、std::unordered_map
尝试了成功,然后惊讶地来到这里。
我认为这可能是因为 push_back
对于某些结构可能比 insert(.end())
更快,但我不确定...
std::list
似乎并非如此(有道理):
// Copying 10,000,000 element-list with std::copy. Did it twice w/ switched order just in case that matters.
Profiling complete (884.666 millis total run-time): inserter(.end())
Profiling complete (643.798 millis total run-time): back_inserter
Profiling complete (644.060 millis total run-time): back_inserter
Profiling complete (623.151 millis total run-time): inserter(.end())
但它对std::vector
有点影响,虽然我不太确定为什么?:
// Copying 10,000,000 element-vector with std::copy.
Profiling complete (985.754 millis total run-time): inserter(.end())
Profiling complete (746.819 millis total run-time): back_inserter
Profiling complete (745.476 millis total run-time): back_inserter
Profiling complete (739.774 millis total run-time): inserter(.end())
我猜想在向量中找出迭代器的位置然后在其中放置一个元素而不是 arr[count++] 会稍微多一些开销。也许是这样?
但是,这还是主要原因吗?
我想我的后续问题是“可以为模板函数编写 std::inserter(container, container.end())
并期望它(几乎)适用于任何 STL 容器吗?”
我在迁移到标准编译器后更新了这些数字。这是我的编译器的详细信息: gcc 版本 4.8.2 (Ubuntu 4.8.2-19ubuntu1) 目标:x86_64-linux-gnu
我的构建命令:
g++ -O0 -std=c++11 algo_test.cc
我认为this question asks the second half of my question,即“我可以编写一个使用std::inserter(container, container.end())
的模板函数并期望它几乎适用于每个容器吗?”
答案是“是的,除了std::forward_list
之外的每个容器。”但是根据下面 cmets 和user2746253 的回答中的讨论,听起来我应该知道std::vector
比使用std::back_inserter
慢...
因此,我可能希望使用RandomAccessIterator
s 专门针对容器模板使用back_inserter
。那有意义吗?谢谢。
【问题讨论】:
back_inserter_iterator
调用push_back
,所以它当然不适用于所有容器。另一方面,insert_iterator
调用 insert
。这些操作的速度取决于您要执行的操作。 works for ANY STL container!!
是错误的。也许C++ vector's insert & push_back difference 会提供信息。
例如std::queue
这两者有不同的要求和保证。例如,std::vector<T>
的 inserter
要求 T
是 MoveAssignable,back_inserter
不是。 Live example
如果您要衡量性能,请不要使用-O0
。
@BenVoigt: std::queue
不是容器,而是容器适配器;例如,它甚至没有begin()
和end()
。
【参考方案1】:
迭代器类型
std::back_inserter
返回使用 Container::push_back()
的 std::back_insert_iterator
。
std::inserter
返回使用 Container::insert()
的 std::insert_iterator
。
std::list
对于列表std::list::push_back
与std::list::insert
几乎相同。唯一的区别是 insert 将迭代器返回到插入的元素。
位/stl_list.h
void push_back(const value_type& __x)
this->_M_insert(end(), __x);
void _M_insert(iterator __position, const value_type& __x)
_Node* __tmp = _M_create_node(__x);
__tmp->_M_hook(__position._M_node);
bits/list.tcc
template<typename _Tp, typename _Alloc> typename list<_Tp, _Alloc>::iterator
list<_Tp, _Alloc>::insert(iterator __position, const value_type& __x)
_Node* __tmp = _M_create_node(__x);
__tmp->_M_hook(__position._M_node);
return iterator(__tmp);
std::vector
std::vector
看起来有点不同。推回检查是否需要重新分配,如果不只是将值放在正确的位置。
位/stl_vector.h
void push_back(const value_type& __x)
if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish, __x);
++this->_M_impl._M_finish;
else
_M_insert_aux(end(), __x);
但是在std::vector::insert
中还做了 3 件额外的事情,这会影响性能。
位/向量.tcc
template<typename _Tp, typename _Alloc> typename vector<_Tp, _Alloc>::iterator
vector<_Tp, _Alloc>::insert(iterator __position, const value_type& __x)
const size_type __n = __position - begin(); //(1)
if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage
&& __position == end()) //(2)
_Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish, __x);
++this->_M_impl._M_finish;
else
_M_insert_aux(__position, __x);
return iterator(this->_M_impl._M_start + __n); //(3)
【讨论】:
我喜欢高级/简洁的解释,归结为push_back()
vs insert()
--> 这回答了问题而不会陷入困境并且仍然足够技术性(即非手波) .
我认为这三个附加命令不会导致很大的性能差异。不同之处在于_M_insert_aux
在push_back
的末尾插入(即它只是一个追加),但它在insert
的中间某处插入,这会导致以下所有数据被推回。 【参考方案2】:
简短的回答是std::insert_iterator
允许您在容器中的任何位置插入:
//insert at index 2
auto it = std::inserter(v, v.begin() + 2);
*it = 4;
为了实现这一点,std::vector 必须将索引 2 之后的现有元素在上面的示例中向下移动一位。这是O(n)
操作,除非您在最后插入,因为没有其他东西可以向下移动。但它仍然需要进行相关检查,这会导致O(1)
perf 惩罚。对于链表,您可以在O(1)
时间的任何位置插入,因此不会受到惩罚。 back_inserter
总是插入到最后,所以也没有任何惩罚。
【讨论】:
以上是关于与 std::inserter 相比,std::back_inserter 有啥好处?的主要内容,如果未能解决你的问题,请参考以下文章
使用 `std::copy()` 和 `std::back_inserter()`