Les vecteurs C++ 11 ont la nouvelle fonction emplace_back
. Contrairement à Push_back
, qui repose sur des optimisations du compilateur pour éviter les copies, emplace_back
utilise une transmission parfaite pour envoyer les arguments directement au constructeur afin de créer un objet sur place. Il me semble que emplace_back
fait tout Push_back
peut faire, mais parfois il fera mieux (mais jamais pire).
Quelle raison dois-je utiliser Push_back
?
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 à propos de Push_back
contre. emplace_back
manque l’image complète.
L'année dernière, j'ai donné une présentation à C++ Now sur déduction de type en C++ 14 . Je commence à parler de Push_back
contre. emplace_back
à 13h49, mais il existe 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 transmettre à 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’ya aucune différence entre ces deux instructions 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
alors que emplace_back
transmettra l'argument et le construira directement sur place, sans copie ni déplacement. Cela peut être vrai si le code est écrit dans des bibliothèques standard, mais cela laisse supposer à 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 en optimisations spécifiques à une plate-forme et que vous ne vous souciez pas de la maintenabilité, mais des performances.
La différence réelle entre ces deux déclarations est que le plus puissant emplace_back
appellera n'importe quel type de constructeur, alors que les plus prudents Push_back
n'appellera que les constructeurs implicites. Les constructeurs implicites sont supposés être en sécurité. Si vous pouvez implicitement construire un U
à partir d'un T
, vous dites que U
peut contenir toutes les informations contenues dans T
sans perte. Dans presque toutes les situations, il est prudent de passer un T
et personne ne s’y intéressera si vous en faites un U
à la 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
to std::uint8_t
.
Nous voulons être prudents dans notre programmation. Nous ne souhaitons pas utiliser de fonctionnalités puissantes, car plus la fonctionnalité est puissante, plus il est facile de faire par inadvertance une action incorrecte ou inattendue. Si vous souhaitez appeler des constructeurs explicites, vous avez besoin de la puissance de emplace_back
. Si vous souhaitez appeler uniquement 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, en passant un pointeur non propriétaire compile parfaitement. Cependant, lorsque v
sort de la portée, le destructeur tente d'appeler delete
sur ce pointeur, qui n'a pas été alloué par new
, car il ne s'agit que d'un objet de pile. Cela conduit à un comportement indéfini.
Ce n'est pas simplement du code inventé. C'était un vrai bug de production que j'ai rencontré. Le code était std::vector<T *>
, mais il en possédait le contenu. Dans le cadre de la migration vers C++ 11, j’ai correctement modifié T *
à std::unique_ptr<T>
pour indiquer que le vecteur possède sa mémoire. Cependant, je basais ces changements sur ma compréhension de 2012, au cours de laquelle je pensais que "emplace_back fait tout ce que Push_back peut faire et bien plus, alors pourquoi devrais-je jamais utiliser Push_back?", Alors j'ai aussi changé le Push_back
à emplace_back
.
Si j'avais laissé le code en utilisant le plus sûr Push_back
, J'aurais instantanément attrapé ce bogue qui aurait été considéré comme une réussite de la mise à niveau vers C++ 11. Au lieu de cela, j'ai masqué le bogue et je ne l'ai trouvé que des mois plus tard.
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.
Compatibilité ascendante avec les compilateurs antérieurs à C++ 11.
Certaines implémentations de bibliothèque de emplace_back ne se comportent pas comme spécifié dans la norme C++, y compris la version livrée avec Visual Studio 2012, 2013 et 2015.
Afin de corriger les bogues connus du compilateur, préférez utiliserstd::vector::Push_back()
si les paramètres font référence à des itérateurs ou à d'autres objets qui ne seront plus 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. Ceci est dû au fait que le deuxième appel à emplace_back
Entraîne un redimensionnement auquel v[0]
Devient invalide.
Une implémentation fonctionnelle 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]);
Remarque: L'utilisation d'un vecteur d'inte est à des fins de démonstration. J'ai découvert ce problème avec une classe beaucoup plus complexe comprenant des variables de membre allouées dynamiquement et l'appel à emplace_back()
entraînant un crash brutal.