与 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::vectorstd::liststd::mapstd::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 慢...

因此,我可能希望使用RandomAccessIterators 专门针对容器模板使用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_iteratorstd::inserter 返回使用 Container::insert()std::insert_iterator

std::list

对于列表std::list::push_backstd::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_auxpush_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::back_inserter函数用法

使用 `std::copy()` 和 `std::back_inserter()`

为啥在 std::copy 期间使用 std::back_inserter 而不是 end()?

仅移动类型的 back_inserter

qt怎么把char[转为vector

std::set 插入器不尊重自定义比较器。 (可能的编译器错误?)