Différence entre make_shared et normal shared_ptr en C ++

std::shared_ptr p1 = std::make_shared("foo"); std::shared_ptr p2(new Object("foo")); 

Beaucoup de publications sur google et stackoverflow sont là, mais je ne suis pas en mesure de comprendre pourquoi make_shared est plus efficace que l’utilisation directe de shared_ptr .

Est-ce que quelqu’un peut m’expliquer la séquence pas à pas des objects créés et des opérations effectuées par les deux afin que je puisse comprendre comment make_shared est efficace. J’ai donné un exemple ci-dessus pour référence.

La différence est que std::make_shared effectue une allocation de tas, alors que l’appel du constructeur std::shared_ptr fait 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 décomptes ref, les effaceurs effacés, etc.)
  • l’object géré

std::make_shared effectue une seule allocation de tas pour l’espace nécessaire à la fois pour le bloc de contrôle et les données. Dans l’autre cas, new Obj("foo") appelle une allocation de tas pour les données gérées et le constructeur std::shared_ptr effectue une autre pour le bloc de contrôle.

Pour plus d’informations, consultez les notes d’implémentation à la cppreference .

Mise à jour I: Exception-Safety

Étant donné que l’OP semble s’interroger sur le côté sécurité des exceptions, j’ai mis à jour ma réponse.

Considérez cet exemple,

 void F(const std::shared_ptr &lhs, const std::shared_ptr &rhs) { /* ... */ } F(std::shared_ptr(new Lhs("foo")), std::shared_ptr(new Rhs("bar"))); 

Parce que C ++ permet l’ordre arbitraire d’évaluation des sous-expressions, un ordre possible est:

  1. new Lhs("foo"))
  2. new Rhs("bar"))
  3. std::shared_ptr
  4. std::shared_ptr

Maintenant, supposons que nous obtenions une exception à l’étape 2 (par exemple, exception hors mémoire, le constructeur de Rhs des exceptions). Nous perdons alors la mémoire allouée à l’étape 1, car rien n’aura eu la chance de le nettoyer. Le cœur du problème ici est que le pointeur brut n’a pas été transmis au constructeur std::shared_ptr immédiatement.

Une façon de résoudre ce problème est de les faire sur des lignes distinctes afin que cet ordre arbitraire ne puisse pas se produire.

 auto lhs = std::shared_ptr(new Lhs("foo")); auto rhs = std::shared_ptr(new Rhs("bar")); F(lhs, rhs); 

Bien sûr, la méthode préférée consiste à utiliser std::make_shared place.

 F(std::make_shared("foo"), std::make_shared("bar")); 

Mise à jour II: Inconvénient de std::make_shared

Citant les commentaires de Casey :

Comme 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é. Un weak_ptr peut maintenir le bloc de contrôle en vie indéfiniment.

Pourquoi les instances de weak_ptr conservent-elles le contrôle du bloc?

Il doit exister un moyen pour weak_ptr s de déterminer si l’object géré est toujours valide (par exemple, pour lock ). Ils le font en vérifiant le nombre de shared_ptr qui possèdent l’object géré, qui est stocké dans le bloc de contrôle. Le résultat est que les blocs de contrôle sont weak_ptr jusqu’à ce que le compte shared_ptr et le compte weak_ptr atteignent tous deux 0.

Retour à std::make_shared

Comme std::make_shared effectue une seule allocation de tas à la fois pour le bloc de contrôle et pour l’object géré, il n’existe aucun moyen de libérer la mémoire pour le bloc de contrôle et l’object géré indépendamment. Nous devons attendre que nous puissions libérer à la fois le bloc de contrôle et l’object géré, jusqu’à ce qu’il n’y ait plus aucun shared_ptr s ou weak_ptr s en vie.

Supposons que nous avons effectué deux allocations de tas pour le bloc de contrôle et l’object géré via le constructeur new et shared_ptr . Ensuite, nous libérons la mémoire pour l’object géré (peut-être plus tôt) lorsqu’il n’y a pas de shared_ptr s en vie, et weak_ptr la mémoire pour le bloc de contrôle (peut-être plus tard) lorsqu’il n’y a pas de weak_ptr vie.

Le pointeur partagé gère à la fois l’object lui-même et un petit object contenant le compte de référence et d’autres données de gestion. make_shared peut allouer un seul bloc de mémoire pour contenir ces deux éléments; La construction d’un pointeur partagé d’un pointeur à un object déjà alloué devra allouer un second bloc pour stocker le compte de référence.

En plus de cette efficacité, l’utilisation de make_shared signifie que vous n’avez pas besoin de manipuler de pointeurs new et bruts, ce qui améliore la sécurité des exceptions – il n’est pas possible de lancer une exception après avoir alloué l’object mais avant de l’atsortingbuer au pointeur intelligent .

Il y a un autre cas où les deux possibilités diffèrent, en plus de celles déjà mentionnées: si vous devez appeler un constructeur non public (protégé ou privé), make_shared pourrait ne pas pouvoir y accéder, alors que la variante avec le nouveau fonctionne bien .

 class A { public: A(): val(0){} std::shared_ptr createNext(){ return std::make_shared(val+1); } // Invalid because make_shared needs to call A(int) **internally** std::shared_ptr createNext(){ return std::shared_ptr(new A(val+1)); } // Works fine because A(int) is called explicitly private: int val; A(int v): val(v){} }; 

Si vous avez besoin d’un alignement de mémoire spécial sur l’object contrôlé par shared_ptr, vous ne pouvez pas compter sur make_shared, mais je pense que c’est la seule bonne raison de ne pas l’utiliser.

Shared_ptr : effectue deux allocations de tas

  1. Bloc de contrôle (compte de référence)
  2. Objet géré

Make_shared : Exécute une seule allocation de tas

  1. Bloc de contrôle et données d’object.

En ce qui concerne l’efficacité et le temps consacré à l’allocation, j’ai fait ce test simple ci-dessous, j’ai créé de nombreuses instances de ces deux manières (une à la fois):

 for (int k = 0 ; k < 30000000; ++k) { // took more time than using new std::shared_ptr foo = std::make_shared (10); // was faster than using make_shared std::shared_ptr foo2 = std::shared_ptr(new int(10)); } 

Le fait est que l’utilisation de make_shared a pris le double du temps par rapport à l’utilisation de new. Donc, en utilisant new, il y a deux allocations de tas au lieu d’un utilisant make_shared. Peut-être que c’est un test stupide mais cela ne montre-t-il pas que l’utilisation de make_shared prend plus de temps que l’utilisation de new? Bien sûr, je ne parle que du temps utilisé.

Je vois un problème avec std :: make_shared, il ne supporte pas les constructeurs privés / protégés