La différence est std::make_shared
qu'effectue une allocation de segment, alors que l'appel du std::shared_ptr
constructeur en effectue deux.
Où les allocations de tas se produisent-elles?
std::shared_ptr
gère deux entités:
- le bloc de contrôle (stocke les métadonnées telles que les comptages de ref, le deleter effacé par type, etc.)
- l'objet géré
std::make_shared
effectue une allocation de segment unique tenant compte de l'espace nécessaire à la fois au bloc de contrôle et aux données. Dans l'autre cas, new Obj("foo")
appelle une allocation de segment pour les données gérées et le std::shared_ptr
constructeur en effectue une autre pour le bloc de contrôle.
Pour plus d'informations, consultez les notes d'implémentation sur cppreference .
Mise à jour I: exception-sécurité
REMARQUE (2019/08/30) : Ce n'est pas un problème depuis C ++ 17, en raison des changements dans l'ordre d'évaluation des arguments de fonction. Plus précisément, chaque argument d'une fonction doit être entièrement exécuté avant l'évaluation des autres arguments.
Étant donné que l'OP semble s'interroger sur le côté exception-sécurité des choses, j'ai mis à jour ma réponse.
Considérez cet exemple,
void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }
F(std::shared_ptr<Lhs>(new Lhs("foo")),
std::shared_ptr<Rhs>(new Rhs("bar")));
Parce que C ++ permet un ordre arbitraire d'évaluation des sous-expressions, un ordre possible est:
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr<Lhs>
std::shared_ptr<Rhs>
Supposons maintenant que nous obtenons une exception levée à l'étape 2 (par exemple, exception de mémoire insuffisante, le Rhs
constructeur a levé une exception). Nous perdons ensuite la mémoire allouée à l'étape 1, car rien n'aura eu l'occasion de le nettoyer. Le cœur du problème ici est que le pointeur brut n'a pas été std::shared_ptr
immédiatement transmis au constructeur.
Une façon de résoudre ce problème est de les faire sur des lignes distinctes afin que cette ordonnance arbitraire ne puisse pas se produire.
auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);
La façon préférée de résoudre ce problème est bien sûr d'utiliser à la std::make_shared
place.
F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));
Mise à jour II: Inconvénient de std::make_shared
Citant les commentaires de Casey :
Puisqu'il n'y a qu'une seule allocation, la mémoire de la pointe ne peut pas être désallouée tant que le bloc de contrôle n'est plus utilisé. A weak_ptr
peut garder le bloc de contrôle en vie indéfiniment.
Pourquoi les instances de weak_ptr
s maintiennent-elles le bloc de contrôle en vie?
Il doit exister un moyen pour weak_ptr
s de déterminer si l'objet géré est toujours valide (par exemple pour lock
). Ils le font en vérifiant le nombre de shared_ptr
s qui possèdent l'objet géré, qui est stocké dans le bloc de contrôle. Le résultat est que les blocs de contrôle sont vivants jusqu'à ce que le shared_ptr
compte et leweak_ptr
compte atteignent tous les deux 0.
Retour à std::make_shared
Puisque std::make_shared
fait une seule allocation de segment pour le bloc de contrôle et l'objet géré, il n'y a aucun moyen de libérer la mémoire pour le bloc de contrôle et l'objet géré indépendamment. Nous devons attendre jusqu'à ce que nous puissions libérer à la fois le bloc de contrôle et l'objet géré, qui se trouve être jusqu'à ce qu'il n'y ait pas de shared_ptr
s ou de weak_ptr
s vivants.
Supposons que nous ayons plutôt effectué deux allocations de tas pour le bloc de contrôle et l'objet géré via new
et shared_ptr
constructeur. Ensuite, nous libérons la mémoire de l'objet géré (peut-être plus tôt) lorsqu'il n'y a pas de shared_ptr
s vivants, et nous libérons la mémoire du bloc de contrôle (peut-être plus tard) lorsqu'il n'y a pas de weak_ptr
s vivants.