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.
std::unique_ptr<T>
a un constructeur explicite deT *
. Parce queemplace_back
peut appeler des constructeurs explicites, passer un pointeur non propriétaire se compile très bien. Cependant, lorsquev
sort du domaine, le destructeur tentera d'appelerdelete
ce pointeur, qui n'a pas été alloué parnew
car il s'agit simplement d'un objet de pile. Cela conduit à un comportement indéfini.explicit
constructeur est un constructeur auquel le mot-clé lui estexplicit
appliqué. Un constructeur "implicite" est tout constructeur qui n'a pas ce mot-clé. Dans le cas dustd::unique_ptr
constructeur deT *
, l'implémenteur de astd::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.push_back
permet toujours l'utilisation d'une initialisation uniforme, ce que j'aime beaucoup. Par exemple:D'un autre côté,
v.emplace_back({ 42, 121 });
ne fonctionnera pas.la source
{}
syntaxe pour appeler un constructeur réel, vous pouvez simplement supprimer le{}
et utiliseremplace_back
.{}
syntaxe pour appeler des constructeurs réels. Vous pourriez donneraggregate
un 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, ceemplace_back
serait préférable, car il appelle le constructeur sur place. Et ne nécessite donc pas que le type soit copiable.emplace
agré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.Compatibilité descendante avec les compilateurs pré-C ++ 11.
la source
push_back
c'est préférable.emplace_back
n'est pas une "grande" version depush_back
. C'est une version potentiellement dangereuse de celui-ci. Lisez les autres réponses.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.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_back
entraîne un redimensionnement auquel pointv[0]
devient invalide.Une implémentation fonctionnelle du code ci-dessus utiliserait
push_back()
au lieu de ceemplace_back()
qui suit: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.la source
push_back
être différent dans ce cas?