web-dev-qa-db-fra.com

Quel est l'avantage de std :: back_inserter sur std :: inserter?

Autant que je sache, n'importe où std::back_inserter fonctionne dans un algorithme STL, vous pouvez passer un std::inserter construit avec .end() à la place:

std::copy(l.begin(), l.end(), std::back_inserter(dest_list));
std::copy(l.begin(), l.end(), std::inserter(dest_list, dest_list.end()));

ET, contrairement à back_inserter, autant que je sache, inserter fonctionne pour TOUT conteneur STL !! Je l'ai essayé avec succès pour std::vector, std::list, std::map, std::unordered_map avant de venir ici surpris.

Je pensais que c'était peut-être parce que Push_back pourrait être plus rapide pour certaines structures que insert(.end()), mais je ne suis pas sûr ...

Cela ne semble pas être le cas pour std::list (est logique):

// 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())

Mais cela fait un peu pour std::vector, bien que je ne sois pas vraiment sûr pourquoi?

// 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())

Je suppose que dans un vecteur, il y a un peu plus de frais généraux pour savoir où se trouve l'itérateur, puis pour y placer un élément par rapport à just arr [count ++]. C'est peut-être ça?

Mais encore, est-ce la raison principale?

Ma question suivante, je suppose, est la suivante: "Peut-on écrire std::inserter(container, container.end()) pour une fonction basée sur un modèle et s’attendre à ce que cela fonctionne pour (presque) tous les conteneurs STL?"


J'ai mis à jour les nombres après avoir migré vers un compilateur standard. Voici les détails de mon compilateur:
gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1) 
Cible: x86_64-linux-gnu

Ma commande de construction:

g++ -O0 -std=c++11 algo_test.cc

Je pense cette question pose la seconde partie de ma question , à savoir: "Puis-je écrire une fonction basée sur un modèle qui utilise std::inserter(container, container.end()) et m'attends à ce qu'elle fonctionne pour presque tous les conteneurs?"

La réponse a été "Oui, pour chaque conteneur à l'exception de std::forward_list." Mais si on se base sur la discussion dans les commentaires ci-dessous et dans la réponse de user2746253 , il semble que je devrais être conscient du fait que cela serait plus lent pour std::vector que d'utiliser std::back_inserter...

Par conséquent, je souhaiterais peut-être spécialiser mon modèle pour les conteneurs utilisant RandomAccessIterators pour utiliser back_inserter à la place. Cela a-t-il du sens? Merci.

42
NHDaly

Types d'itérateur

  • std::back_inserter renvoie std::back_insert_iterator qui utilise Container::Push_back().
  • std::inserter renvoie std::insert_iterator qui utilise Container::insert().

std :: list

Pour les listes, std::list::Push_back est presque identique à std::list::insert. La seule différence est que insert renvoie l'itérateur à l'élément inséré.

bits/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

Cela semble un peu différent pour std::vector. Repousser les vérifications si la réaffectation est nécessaire, et sinon ne place pas la valeur au bon endroit.

bits/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);
  }

Mais dans std::vector::insert il y a 3 choses supplémentaires effectuées et cela a un impact sur les performances . Bits/vector.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)
  }
49
user2746253

La réponse courte est que std::insert_iterator vous permet d'insérer n'importe où dans le conteneur:

//insert at index 2
auto it = std::inserter(v, v.begin() + 2);
*it = 4;

Pour ce faire, std :: vector doit déplacer les éléments existants après l'index 2 d'une place dans l'exemple ci-dessus. Ceci est l'opération O(n) sauf si vous insérez à la fin car il n'y a rien d'autre à faire descendre. Mais il doit encore effectuer des vérifications pertinentes qui entraînent une pénalité de O(1) perf. Pour les listes chaînées, vous pouvez insérer n'importe où dans O(1) time pour éviter toute pénalité. Le back_inserter insère toujours à la fin, donc pas de pénalité non plus.

0
Shital Shah