Introduction : ce que les documentations taisent
Imaginez : vous écrivez un serveur à forte charge ou un algorithme de traitement de données. Vous avez deux std::vector<int>, et vous devez les fusionner en un seul. Rien de plus simple, semble-t-il ? Il suffit de prendre et d'ajouter. Mais C++ ne pardonne pas l'approche superficielle. Une mauvaise méthode, et vous obtenez des copies superflues, une baisse de performance, voire un comportement indéfini.
La question « Appending a vector to a vector » sur Stack Overflow a recueilli des milliers de vues pour une bonne raison. Même les développeurs expérimentés confondent parfois push_back avec insert ou oublient la sémantique de déplacement. Dans cet article, nous allons regarder sous le capot du compilateur et comprendre comment fonctionne réellement l'ajout d'un vecteur à un autre. Vous découvrirez pourquoi std::copy peut être plus rapide que insert, et quand la sémantique de déplacement peut sauver votre projet.
La méthode la plus évidente (et dangereuse) : push_back dans une boucle
Les débutants écrivent souvent ceci :
std::vector<int> dest = {1, 2, 3};std::vector<int> src = {4, 5, 6};
for (size_t i = 0; i < src.size(); ++i) { dest.push_back(src[i]);}Que se passe-t-il ici ? À chaque itération, push_back vérifie si la mémoire allouée est suffisante. Si ce n'est pas le cas, une réallocation se produit : l'ancien tableau est copié dans une nouvelle zone mémoire, l'ancien est supprimé. Avec trois éléments, il peut y avoir 2 à 3 réallocations. Pour les petits vecteurs, c'est imperceptible, mais avec des milliers d'éléments, c'est une catastrophe.
Problème : O(n) vérifications et copie potentielle de l'intégralité du tableau. En C++17 et versions ultérieures, le compilateur peut optimiser, mais compter là-dessus est risqué.
Que se passe-t-il sous le capot de push_back ?
- Vérification de
size() < capacity(). - Si non, appel à
reallocate(new_capacity). - Copie (ou déplacement) des éléments existants.
- Suppression de l'ancien tampon.
- Copie du nouvel élément.
Conclusion : n'utilisez jamais une boucle pour ajouter un vecteur entier, sauf si vous le faites délibérément pour un petit nombre d'éléments.
La bonne voie : std::vector::insert
La bibliothèque standard fournit une solution élégante :
std::vector<int> dest = {1, 2, 3};std::vector<int> src = {4, 5, 6};
dest.insert(dest.end(), src.begin(), src.end());Une ligne ! Mais que se cache-t-il derrière cet appel ? insert fait ce qui suit :
- Calcule la quantité de mémoire nécessaire :
dest.size() + src.size(). - Si la capacité actuelle est insuffisante, alloue exactement ce qui est nécessaire (ou avec une marge, selon l'implémentation).
- Décale les éléments existants (si l'insertion n'est pas à la fin).
- Copie (ou déplace) les nouveaux éléments depuis
src.
Important : insert effectue une seule réallocation (ou aucune si l'espace est suffisant). C'est déjà mieux qu'une boucle, mais il y a toujours une copie des éléments de src.
Performances de insert
Si src n'est plus nécessaire après l'insertion, vous pouvez déplacer ses éléments en utilisant std::make_move_iterator :
dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));Cela transforme la copie en déplacement. Pour std::vector<int>, il n'y a pas de différence (int est un type trivial), mais pour std::vector<std::string> ou des objets complexes, le déplacement est une broutille comparé à la copie.
Alternative : std::copy avec back_inserter
Certains développeurs préfèrent les algorithmes STL :
Blogs
Book Recommendations
Lire aussi