Pourquoi utiliserais-je push_back au lieu de emplace_back?

232

Les vecteurs C ++ 11 ont la nouvelle fonction emplace_back. Contrairement à push_back, qui repose sur les optimisations du compilateur pour éviter les copies, emplace_backutilise un transfert parfait pour envoyer les arguments directement au constructeur pour créer un objet sur place. Il me semble que emplace_backtout push_backpeut faire, mais parfois il le fera mieux (mais jamais pire).

Quelle raison dois-je utiliser push_back?

David Stone
la source

Réponses:

162

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_backvs emplace_backmanquent 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_backvs 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_backou 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_backva construire un objet temporaire, qui sera ensuite déplacé dans vtandis que emplace_backfera 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_backappellent n'importe quel type de constructeur, alors que les plus prudents push_backn'appelleront que les constructeurs implicites. Les constructeurs implicites sont censés être sûrs. Si vous pouvez implicitement construire un à Upartir d'un T, vous dites qu'il Upeut contenir toutes les informations Tsans perte. Il est sûr dans à peu près n'importe quelle situation de passer Tet personne ne s'en souciera si vous en faites un à la Uplace. Un bon exemple de constructeur implicite est la conversion de std::uint32_tà std::uint64_t. Un mauvais exemple de conversion implicite est doublede 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_backpeut appeler des constructeurs explicites, passer un pointeur non propriétaire se compile très bien. Cependant, lorsque vsort du domaine, le destructeur tentera d'appeler deletece pointeur, qui n'a pas été alloué par newcar 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_backpour 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.

David Stone
la source
Il serait utile que vous expliquiez ce que fait exactement dans votre exemple, et pourquoi c'est faux.
eddi
4
@eddi: J'ai ajouté une section expliquant ceci: std::unique_ptr<T>a un constructeur explicite de T *. Parce que emplace_backpeut appeler des constructeurs explicites, passer un pointeur non propriétaire se compile très bien. Cependant, lorsque vsort du domaine, le destructeur tentera d'appeler deletece pointeur, qui n'a pas été alloué par newcar il s'agit simplement d'un objet de pile. Cela conduit à un comportement indéfini.
David Stone
Merci d'avoir posté ça. Je ne le savais pas quand j'ai écrit ma réponse mais maintenant j'aurais aimé l'écrire moi-même quand je l'ai appris par la suite :) Je veux vraiment gifler les gens qui passent à de nouvelles fonctionnalités juste pour faire la chose la plus branchée qu'ils peuvent trouver . Les gars, les gens utilisaient C ++ avant C ++ 11 aussi, et tout n'était pas problématique. Si vous ne savez pas pourquoi vous utilisez une fonctionnalité, ne l'utilisez pas . Je suis tellement content que vous ayez posté cela et j'espère que cela augmentera le nombre de votes afin qu'il dépasse le mien. +1
user541686
1
@CaptainJacksparrow: On dirait que je dis implicite et explicite où je les veux dire. Quelle partie avez-vous confondu?
David Stone
4
@CaptainJacksparrow: Un explicitconstructeur est un constructeur auquel le mot-clé lui est explicitappliqué. Un constructeur "implicite" est tout constructeur qui n'a pas ce mot-clé. Dans le cas du std::unique_ptrconstructeur de T *, l'implémenteur de a std::unique_ptrécrit ce constructeur, mais le problème ici est que l'utilisateur de ce type a appelé emplace_back, qui a appelé ce constructeur explicite. S'il l'avait été push_back, au lieu d'appeler ce constructeur, il se serait appuyé sur une conversion implicite, qui ne peut appeler que des constructeurs implicites.
David Stone
117

push_backpermet toujours l'utilisation d'une initialisation uniforme, ce que j'aime beaucoup. Par exemple:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.push_back({ 42, 121 });

D'un autre côté, v.emplace_back({ 42, 121 });ne fonctionnera pas.

Luc Danton
la source
58
Notez que cela ne s'applique qu'à l'initialisation agrégée et à l'initialisation de la liste des initialiseurs. Si vous souhaitez utiliser la {}syntaxe pour appeler un constructeur réel, vous pouvez simplement supprimer le {}et utiliser emplace_back.
Nicol Bolas
Temps de question stupide: donc emplace_back ne peut pas du tout être utilisé pour des vecteurs de structures? Ou tout simplement pas pour ce style utilisant le littéral {42,121}?
Phil H
1
@LucDanton: Comme je l'ai dit, cela ne s'applique qu'à l' agrégation d' agrégats et d' initialisation de liste . Vous pouvez utiliser la {}syntaxe pour appeler des constructeurs réels. Vous pourriez donner aggregateun constructeur qui prend 2 entiers, et ce constructeur serait appelé lors de l'utilisation de la {}syntaxe. Le fait est que si vous essayez d'appeler un constructeur, ce emplace_backserait préférable, car il appelle le constructeur sur place. Et ne nécessite donc pas que le type soit copiable.
Nicol Bolas
11
Cela a été considéré comme un défaut dans la norme et a été résolu. Voir cplusplus.github.io/LWG/lwg-active.html#2089
David Stone
3
@DavidStone S'il avait été résolu, il ne serait pas encore dans la liste "active" ... non? Cela semble rester une question en suspens. La dernière mise à jour, intitulée " [2018-08-23 Batavia Issues processing] ", dit que " P0960 (actuellement en vol) devrait résoudre ce problème. " Et je ne peux toujours pas compiler de code qui essaie d' emplaceagréger sans écrire explicitement un constructeur standard. . Il est également difficile de savoir à ce stade s'il sera traité comme un défaut et donc éligible pour le rétroportage, ou si les utilisateurs de C ++ <20 resteront SoL.
underscore_d
80

Compatibilité descendante avec les compilateurs pré-C ++ 11.

user541686
la source
21
Cela semble être la malédiction du C ++. Nous obtenons des tonnes de fonctionnalités intéressantes à chaque nouvelle version, mais de nombreuses entreprises sont bloquées en utilisant une ancienne version pour des raisons de compatibilité ou en décourageant (sinon interdisant) l'utilisation de certaines fonctionnalités.
Dan Albert
6
@Mehrdad: Pourquoi se contenter de suffisamment alors que vous pouvez en avoir beaucoup? Je ne voudrais certainement pas programmer en blub , même si c'était suffisant. Cela ne veut pas dire que c'est le cas pour cet exemple en particulier, mais en tant que personne qui passe la plupart de son temps à programmer en C89 pour des raisons de compatibilité, c'est certainement un vrai problème.
Dan Albert
3
Je ne pense pas que ce soit vraiment une réponse à la question. Pour moi, il demande des cas d'utilisation où push_backc'est préférable.
M. Boy
3
@ Mr.Boy: C'est préférable lorsque vous voulez être rétrocompatible avec les compilateurs pré-C ++ 11. Cela n'était-il pas clair dans ma réponse?
user541686
6
Cela a attiré beaucoup plus d'attention que je ne le pensais, donc pour vous tous qui lisez ceci: emplace_backn'est pas une "grande" version de push_back. C'est une version potentiellement dangereuse de celui-ci. Lisez les autres réponses.
user541686
68

Certaines implémentations de bibliothèque d'emplace_back ne se comportent pas comme spécifié dans la norme C ++, y compris la version fournie avec Visual Studio 2012, 2013 et 2015.

Afin d'accommoder les bogues connus du compilateur, préférez utiliser std::vector::push_back()si les paramètres font référence à des itérateurs ou à d'autres objets qui ne seront pas valides après l'appel.

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers

Sur un compilateur, v contient les valeurs 123 et 21 au lieu des 123 et 123 attendus. Cela est dû au fait que le 2e appel à emplace_backentraîne un redimensionnement auquel point v[0]devient invalide.

Une implémentation fonctionnelle du code ci-dessus utiliserait push_back()au lieu de ce emplace_back()qui suit:

std::vector<int> v;
v.emplace_back(123);
v.push_back(v[0]);

Remarque: L'utilisation d'un vecteur d'entiers est à des fins de démonstration. J'ai découvert ce problème avec une classe beaucoup plus complexe qui comprenait des variables membres allouées dynamiquement et l'appel à emplace_back()entraînait un plantage brutal.

Marc
la source
Attendre. Cela semble être un bug. Comment peut-il push_backêtre différent dans ce cas?
balki
12
L'appel à emplace_back () utilise un transfert parfait pour effectuer la construction en place et en tant que tel, v [0] n'est évalué qu'après que le vecteur a été redimensionné (auquel point v [0] n'est pas valide). push_back construit le nouvel élément et copie / déplace l'élément selon les besoins et v [0] est évalué avant toute réallocation.
Marc
1
@Marc: La norme garantit que emplace_back fonctionne même pour les éléments à l'intérieur de la plage.
David Stone
1
@DavidStone: Pourriez-vous s'il vous plaît fournir une référence à l'endroit où dans la norme ce comportement est garanti? Dans les deux cas, Visual Studio 2012 et 2015 présentent un comportement incorrect.
Marc
4
@cameino: emplace_back existe pour retarder l'évaluation de son paramètre afin de réduire la copie inutile. Le comportement est soit indéfini, soit un bogue du compilateur (en attente d'analyse de la norme). J'ai récemment exécuté le même test contre Visual Studio 2015 et obtenu 123,3 sous Release x64, 123,40 sous Release Win32 et 123, -572662307 sous Debug x64 et Debug Win32.
Marc