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.
std::shared_ptr
gère deux entités:
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 .
É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:
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr
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"));
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.
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.
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
Make_shared
: Exécute une seule allocation de tas
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