Pourquoi j'utiliserais push back au lieu de emplace back?

C++11 vecteurs ont la nouvelle fonction emplace_back . Contrairement à push_back , qui s'appuie sur les optimisations des compilateurs pour éviter les copies, emplace_back utilise la transmission parfaite pour envoyer les arguments directement au constructeur pour créer un objet en place. Il me semble que emplace_back fait tout ce que push_back peut faire, mais parfois il le fera mieux (mais jamais pire).

Quelle raison dois-je utiliser push_back ?

176
demandé sur ildjarn 2012-06-05 06:01:45

4 réponses

push_back permet toujours l'utilisation de l'initialisation uniforme, ce que j'aime beaucoup. Par exemple:

struct aggregate {
    int foo;
    int bar;
};

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

par contre, v.emplace_back({ 42, 121 }); ne fonctionnera pas.

103
répondu Luc Danton 2012-06-05 21:39:36

j'ai beaucoup réfléchi à cette question au cours des quatre dernières années. Je suis arrivé à la conclusion que la plupart des explications sur push_back vs. emplace_back manquent le tableau complet.

L'année dernière, j'ai donné une présentation à C++maintenant sur déduction de Type dans 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 primaire concerne les constructeurs implicites et les constructeurs 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);

après que votre compilateur d'optimisation ait mis la main dessus, il n'y a pas de différence entre ces deux énoncés en termes de code généré. La sagesse traditionnelle est que push_back construira un objet temporaire, qui sera ensuite déplacé dans v attendu que emplace_back acheminera l'argument et le construira directement en place sans copies ni mouvements. Cela peut être vrai en se basant sur le code tel qu'écrit dans les bibliothèques standards, mais cela fait l'hypothèse erronée que l'optimisation du travail du compilateur est de générer le code que vous avez écrit. Le travail du compilateur d'optimisation est en fait de générer le code que vous auriez écrit Si vous étiez un expert des optimisations spécifiques à la plate-forme et ne vous souciez pas de la maintenabilité, juste performance.

la différence réelle entre ces deux affirmations est que le plus puissant emplace_back appellera n'importe quel type de constructeur là-bas, tandis que le plus prudent push_back appellera seulement les constructeurs qui sont implicites. Les constructeurs implicites sont censés être sûrs. Si vous pouvez implicitement construire un U à partir d'un T , vous dites que U peut contenir toute l'information dans T sans perte. Il est en sécurité dans à peu près n'importe quelle situation pour passer un T et personne ne s'en souciera si vous faites un U à la place. Un bon exemple de constructeur implicite est la conversion de std::uint32_t en std::uint64_t . Un mauvais exemple de conversion implicite est double en std::uint8_t .

Nous voulons être prudents dans notre programmation. Nous ne voulons pas utiliser des fonctionnalités puissantes parce que plus la fonctionnalité est puissante, plus il est facile de faire accidentellement quelque chose de incorrect ou inattendu. Si vous avez l'intention d'appeler des constructeurs explicites, alors vous avez besoin de la puissance de emplace_back . Si vous voulez appeler seulement les constructeurs implicites, s'en tenir à 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, en passant un pointeur non propriétaire compile très bien. Toutefois, lorsque v va hors de portée, le destructeur tentera d'appeler delete sur ce pointeur, qui n'a pas été attribué par new parce que c'est juste un objet de pile. Cela conduit à un comportement indéfini.

ce n'est pas qu'un code inventé. C'était un vrai bug de production que j'ai rencontré. Le code était std::vector<T *> , mais il possédait le contenu. Dans le cadre de la migration vers C++11, j'ai correctement changé T * en std::unique_ptr<T> pour indiquer que le vecteur possédait sa mémoire. Cependant, J'ai basé ces changements sur ma compréhension en 2012, au cours de laquelle je me suis dit " emplace_back fait tout ce que push_back peut faire et plus encore, alors pourquoi est-ce que j'utiliserais push_back?", donc j'ai aussi changé le push_back en emplace_back .

si j'avais plutôt quitté le code en utilisant le plus sûr push_back , j'aurais instantanément attrapé 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 ça, j'ai masqué le bug et je ne l'ai pas trouvé avant des mois. tard.

86
répondu David Stone 2017-10-31 22:21:29

Rétro-compatibilité avec les pré-C++11 compilateurs.

78
répondu Mehrdad 2012-06-05 02:03:21

certaines implémentations de bibliothèque d'emplace_back ne se comportent pas comme spécifié dans la norme C++ incluant la version qui est envoyée avec Visual Studio 2012, 2013 et 2015.

afin d'accommoder les bogues connus du compilateur, il est préférable d'utiliser std::vector::push_back() si les paramètres renvoient à des itérateurs ou à d'autres objets qui seront invalides 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 2ème appel à emplace_back se traduit par un redimensionnement à partir duquel v[0] devient invalide.

une application pratique du code ci-dessus utiliserait push_back() au lieu de emplace_back() comme suit:

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

Note: l'utilisation d'un vecteur d'ints est à des fins de démonstration. J'ai découvert ce problème avec une classe beaucoup plus complexe qui incluait des variables membres allouées dynamiquement et l'appel à emplace_back() a causé un accident.

63
répondu Marc 2016-02-11 18:08:01