J'ai beaucoup réfléchi à cette question au cours des quatre dernières années. J'en suis arrivé à la conclusion que la plupart des explications concernant push_back
vs emplace_back
manquent l'image complète.
L'année dernière, j'ai fait une présentation à C ++ Now sur la déduction de type en C ++ 14 . Je commence à parler de push_back
vs emplace_back
à 13:49, mais il y a des informations utiles qui fournissent des preuves à l'appui avant cela.
La vraie différence principale concerne les constructeurs implicites et explicites. Considérons le cas où nous avons un seul argument que nous voulons passer à push_back
ou emplace_back
.
std::vector<T> v;
v.push_back(x);
v.emplace_back(x);
Une fois que votre compilateur d'optimisation a mis la main dessus, il n'y a aucune différence entre ces deux instructions en termes de code généré. La sagesse traditionnelle est que push_back
va construire un objet temporaire, qui sera ensuite déplacé dans v
tandis que emplace_back
fera avancer l'argument et le construira directement en place sans copie ni mouvement. Cela peut être vrai en fonction du code tel qu'il est écrit dans les bibliothèques standard, mais cela suppose à tort que le travail du compilateur d'optimisation consiste à générer le code que vous avez écrit. Le travail du compilateur d'optimisation consiste en fait à générer le code que vous auriez écrit si vous étiez un expert des optimisations spécifiques à la plate-forme et que vous ne vous souciez pas de la maintenabilité, mais uniquement des performances.
La différence réelle entre ces deux déclarations est que les plus puissants emplace_back
appellent n'importe quel type de constructeur, alors que les plus prudents push_back
n'appelleront que les constructeurs implicites. Les constructeurs implicites sont censés être sûrs. Si vous pouvez implicitement construire un à U
partir d'un T
, vous dites qu'il U
peut contenir toutes les informations T
sans perte. Il est sûr dans à peu près n'importe quelle situation de passer T
et personne ne s'en souciera si vous en faites un à la U
place. Un bon exemple de constructeur implicite est la conversion de std::uint32_t
à std::uint64_t
. Un mauvais exemple de conversion implicite est double
de std::uint8_t
.
Nous voulons être prudents dans notre programmation. Nous ne voulons pas utiliser des fonctionnalités puissantes car plus la fonctionnalité est puissante, plus il est facile de faire accidentellement quelque chose de incorrect ou d'inattendu. Si vous avez l'intention d'appeler des constructeurs explicites, vous avez besoin de la puissance de emplace_back
. Si vous ne souhaitez appeler que des constructeurs implicites, respectez la sécurité de push_back
.
Un exemple
std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.push_back(std::addressof(a)); // fails to compile
std::unique_ptr<T>
a un constructeur explicite de T *
. Parce que emplace_back
peut appeler des constructeurs explicites, passer un pointeur non propriétaire se compile très bien. Cependant, lorsque v
sort du domaine, le destructeur tentera d'appeler delete
ce pointeur, qui n'a pas été alloué par new
car il s'agit simplement d'un objet de pile. Cela conduit à un comportement indéfini.
Ce n'est pas seulement du code inventé. C'était un vrai bug de production que j'ai rencontré. Le code l'était std::vector<T *>
, mais il en possédait le contenu. Dans le cadre de la migration vers C ++ 11, j'ai changé correctement T *
à std::unique_ptr<T>
indiquer que le vecteur possédait sa mémoire. Cependant, je basais ces changements sur ma compréhension en 2012, au cours de laquelle je pensais que "emplace_back fait tout ce que push_back peut faire et plus encore, alors pourquoi devrais-je jamais utiliser push_back?", J'ai donc également changé le push_back
pour emplace_back
.
Si j'avais plutôt laissé le code utiliser le plus sûr push_back
, j'aurais instantanément détecté ce bogue de longue date et il aurait été considéré comme un succès de la mise à niveau vers C ++ 11. Au lieu de cela, j'ai masqué le bug et ne l'ai trouvé que des mois plus tard.