Comment effacer un élément de std::vector par index : plongée sous le capot
\n\nSalut, collègue ! Si tu as déjà codé en C++ et manipulé des tableaux dynamiques, tu t'es sûrement posé la question : "How do I erase an element from std::vector<> by index?" — c'est l'une des requêtes les plus fréquentes sur Stack Overflow, et pour cause. À première vue, la tâche semble triviale : tu appelles erase et le tour est joué. Mais que se passe-t-il réellement en mémoire ? Pourquoi la suppression au milieu d'un vecteur coûte-t-elle cher ? Et comment éviter les pièges avec les itérateurs ?
Dans cet article, nous ne nous contenterons pas de montrer comment écrire v.erase(v.begin() + index). Nous allons comprendre comment cette opération fonctionne sous le capot, quelles structures de données sont impliquées et comment choisir la méthode de suppression optimale. C'est parti !
1. std::vector : de quoi s'agit-il ?
\n\nAvant de supprimer, rappelons comment std::vector est conçu. C'est un tableau dynamique qui stocke les éléments dans une zone mémoire contiguë. Il possède trois pointeurs (ou itérateurs) clés :
- \n
begin— le début de la mémoire allouée ; \n end— la fin de la mémoire utilisée (après le dernier élément) ; \n capacity— la fin de la mémoire allouée (limite jusqu'à laquelle on peut grandir sans réallocation). \n
Visuellement, cela ressemble à ceci :
\n[0] [1] [2] [3] [4] ... [capacity-1]\n ^ ^\n begin end\n\n\nLorsque tu supprimes un élément au milieu, le vecteur ne se contente pas de le "jeter" — il décale physiquement tous les éléments suivants vers la gauche. C'est là le principal "point douloureux" de l'opération erase.
2. Suppression par index : syntaxe de base
\n\nLa façon la plus simple de supprimer un élément par index est d'utiliser la méthode erase avec un itérateur :
#include <vector>\n#include <iostream>\n\nint main() {\n std::vector<int> v = {10, 20, 30, 40, 50};\n int index = 2; // on veut supprimer le troisième élément (30)\n\n // Vérification que l'index est dans les limites\n if (index < v.size()) {\n v.erase(v.begin() + index);\n }\n\n for (int x : v) std::cout << x << " ";\n // Affichage : 10 20 40 50\n return 0;\n}\n\n\nSimple, non ? Mais que faire si tu dois supprimer plusieurs éléments consécutifs ? Il existe une surcharge erase(first, last) :
v.erase(v.begin() + 1, v.begin() + 3); // supprime les éléments aux indices 1 et 2\n\n\n3. Et sous le capot ? Analyse détaillée
\n\nPassons maintenant à la partie intéressante. Quand tu appelles v.erase(iterator), voici ce qui se passe en interne :
- \n
- Vérification de l'itérateur (en mode debug) — on s'assure qu'il est valide et pointe sur un élément du vecteur. \n
- Décalage des éléments — tous les éléments situés après l'élément supprimé sont copiés (ou déplacés, si le type supporte la sémantique de mouvement) d'une position vers la gauche. \n
- Réduction de la taille — le compteur interne
sizeest diminué de 1. \n - Destructeur — pour l'élément "de queue" (qui est maintenant dupliqué), le destructeur est appelé (si le type n'est pas trivial). \n
Regardons une implémentation simplifiée (pseudo-code) :
\n\ntemplate<typename T>\ntypename std::vector<T>::iterator\nvector<T>::erase(iterator pos) {\n // 1. Décaler tous les éléments après pos d'une position vers la gauche\n for (auto it = pos; it != end() - 1; ++it) {\n *it = std::move(*(it + 1));\n }\n // 2. Réduire la taille\n --size;\n // 3. Appeler le destructeur pour le dernier élément\n allocator.destroy(end()); // end() pointe maintenant sur l'ancien dernier élément\n return pos; // retourne un itérateur sur l'élément suivant celui supprimé\n}\n\n\nRemarque l'utilisation de std::move — le C++ moderne essaie d'éviter les copies inutiles. Cette optimisation est cruciale pour les types coûteux à copier.