Introduction : pourquoi est-ce important ?
\n\nAvez-vous déjà réfléchi à la raison pour laquelle il existe deux façons d'ajouter un élément à la fin d'un vecteur en C++ ? Apparemment, pourquoi réinventer la roue alors qu'il existe le push_back éprouvé ? La réponse réside dans les performances et la flexibilité du C++ moderne.
Sur Stack Overflow, cette question a recueilli des milliers de vues et des centaines de commentaires. Et ce n'est pas un hasard : comprendre la différence entre push_back et emplace_back n'est pas seulement une connaissance académique, mais une compétence pratique qui peut accélérer votre code de 2 à 3 fois. Décortiquons cela une fois pour toutes.
Différence fondamentale : que disent les documentations ?
\n\nCommençons par le simple. Les deux méthodes ajoutent un élément à la fin du conteneur (vecteur, liste, file d'attente). Mais elles le font différemment :
\n\n- \n
push_back— prend un objet prêt (par valeur, référence ou référence rvalue) et le copie/déplace dans le conteneur. \n emplace_back— prend les arguments du constructeur de l'objet et le crée directement à l'intérieur du conteneur (construction in-place). \n
Cela semble abstrait ? Regardons le code.
\n\nExemple pratique : création d'une classe
\n\nImaginons que nous ayons une classe simple Person :
#include <iostream>\n#include <vector>\n#include <string>\n\nclass Person {\npublic:\n std::string name;\n int age;\n\n // Constructeur\n Person(const std::string& n, int a) \n : name(n), age(a) {\n std::cout << "Constructeur appelé pour " << name << std::endl;\n }\n\n // Constructeur de copie\n Person(const Person& other) \n : name(other.name), age(other.age) {\n std::cout << "Constructeur de copie pour " << name << std::endl;\n }\n\n // Constructeur de déplacement\n Person(Person&& other) noexcept\n : name(std::move(other.name)), age(other.age) {\n std::cout << "Constructeur de déplacement pour " << name << std::endl;\n }\n};\n\nVoyons maintenant ce qui se passe avec différentes méthodes.
\n\nOption 1 : push_back avec un objet temporaire
\n\nint main() {\n std::vector<Person> people;\n people.push_back(Person("Alice", 25));\n return 0;\n}\n\nSortie :
\nConstructeur appelé pour Alice\nConstructeur de déplacement pour Alice\n\nQue s'est-il passé ? D'abord, un objet temporaire Person est créé (premier appel au constructeur). Ensuite, cet objet est déplacé dans le vecteur (constructeur de déplacement). Deux appels — double travail.
Option 2 : push_back avec un objet prêt
\n\nint main() {\n std::vector<Person> people;\n Person alice("Alice", 25);\n people.push_back(alice); // alice — lvalue\n return 0;\n}\n\nSortie :
\nConstructeur appelé pour Alice\nConstructeur de copie pour Alice\n\nIci, nous voyons une copie, et non un déplacement. Si l'objet est volumineux (contenant par exemple des tableaux dynamiques), la copie peut être coûteuse.
\n\nOption 3 : emplace_back
\n\nint main() {\n std::vector<Person> people;\n people.emplace_back("Bob", 30); // On passe les arguments du constructeur\n return 0;\n}\n\nSortie :
\nConstructeur appelé pour Bob\n\nUn seul appel au constructeur ! L'objet Person est créé directement dans la mémoire du vecteur, sans copies ou déplacements intermédiaires.
Performances : quand la différence est-elle critique ?
\n\nPour les types simples (int, double, pointeurs), la différence est imperceptible — le compilateur optimise tout. Mais pour les objets complexes, la différence peut être significative :
\n\n- \n
- Objets avec un constructeur de copie lourd (par exemple,
std::string, conteneu