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
?
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.
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.
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.