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

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

De nombreux articles sur Google et stackoverflow sont là-dessus, mais je ne peux pas comprendre pourquoi make_sharedest plus efficace que l'utilisation directe shared_ptr.

Quelqu'un peut-il m'expliquer étape par étape la séquence d'objets créés et les opérations effectuées par les deux afin que je puisse comprendre comment make_sharedest efficace. J'ai donné un exemple ci-dessus pour référence.

Anup Buchke
la source
4
Ce n'est pas plus efficace. La raison de l'utiliser est pour une sécurité exceptionnelle.
Yuushi
Certains articles disent que cela évite certains frais généraux de construction, pouvez-vous s'il vous plaît expliquer plus à ce sujet?
Anup Buchke
16
@Yuushi: La sécurité d'exception est une bonne raison de l'utiliser, mais elle est également plus efficace.
Mike Seymour
3
32:15, c'est là qu'il commence dans la vidéo que j'ai liée à ci-dessus, si cela aide.
chris
4
Avantage mineur du style de code: en utilisant, make_sharedvous pouvez écrire auto p1(std::make_shared<A>())et p1 aura le type correct.
Ivan Vergiliev

Réponses:

333

La différence est std::make_sharedqu'effectue une allocation de segment, alors que l'appel du std::shared_ptrconstructeur 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_sharedeffectue 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_ptrconstructeur 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:

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

Supposons maintenant que nous obtenons une exception levée à l'étape 2 (par exemple, exception de mémoire insuffisante, le Rhsconstructeur 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_ptrimmé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_sharedplace.

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_ptrpeut garder le bloc de contrôle en vie indéfiniment.

Pourquoi les instances de weak_ptrs maintiennent-elles le bloc de contrôle en vie?

Il doit exister un moyen pour weak_ptrs 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_ptrs 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_ptrcompte et leweak_ptr compte atteignent tous les deux 0.

Retour à std::make_shared

Puisque std::make_sharedfait 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_ptrs ou de weak_ptrs vivants.

Supposons que nous ayons plutôt effectué deux allocations de tas pour le bloc de contrôle et l'objet géré via newet shared_ptrconstructeur. 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_ptrs 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_ptrs vivants.

mpark
la source
53
C'est une bonne idée de mentionner également le petit inconvénient du boîtier de coin make_shared: puisqu'il n'y a qu'une seule allocation, la mémoire de la pointe ne peut pas être désallouée jusqu'à ce que le bloc de contrôle ne soit plus utilisé. A weak_ptrpeut maintenir le bloc de contrôle en vie indéfiniment.
Casey
14
Un autre point, plus stylistique, est: si vous utilisez make_sharedet de manière make_uniquecohérente, vous n'aurez pas de pointeurs bruts et vous pourrez traiter chaque occurrence newcomme une odeur de code.
Philipp
6
S'il n'y en a qu'un shared_ptr, et pas de weak_ptrs, l'appel reset()à l' shared_ptrinstance supprimera le bloc de contrôle. Mais c'est indépendamment ou si a make_sharedété utilisé. L'utilisation make_sharedfait une différence car elle peut prolonger la durée de vie de la mémoire allouée à l'objet géré . Lorsque le shared_ptrnombre atteint 0, le destructeur de l'objet géré est appelé indépendamment make_shared, mais la libération de sa mémoire ne peut être effectuée que si elle make_sharedn'a pas été utilisée. J'espère que cela le rendra plus clair.
mpark
4
Il convient également de mentionner que make_shared peut tirer parti de l'optimisation "Nous savons où vous vivez" qui permet au bloc de contrôle d'être un pointeur plus petit. (Pour plus de détails, voir la présentation GN2012 de Stephan T. Lavavej à environ la minute 12.) make_shared évite ainsi non seulement une allocation, mais alloue également moins de mémoire totale.
KnowItAllWannabe
1
@HannaKhalil: Est-ce peut-être le domaine de ce que vous recherchez ...? melpon.org/wandbox/permlink/b5EpsiSxDeEz8lGH
mpark
26

Le pointeur partagé gère à la fois l'objet lui-même et un petit objet contenant le nombre de références et d'autres données de gestion. make_sharedpeut allouer un seul bloc de mémoire pour contenir les deux; la construction d'un pointeur partagé à partir d'un pointeur vers un objet déjà alloué devra allouer un deuxième bloc pour stocker le nombre de références.

En plus de cette efficacité, l'utilisation make_sharedsignifie que vous n'avez pas besoin de traiter avec newdes pointeurs bruts, ce qui donne une meilleure sécurité d'exception - il n'y a aucune possibilité de lever une exception après avoir alloué l'objet mais avant de l'affecter au pointeur intelligent.

Mike Seymour
la source
2
J'ai bien compris votre premier point. Pouvez-vous développer ou donner des liens sur le deuxième point concernant la sécurité des exceptions?
Anup Buchke
22

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

class A
{
public:

    A(): val(0){}

    std::shared_ptr<A> createNext(){ return std::make_shared<A>(val+1); }
    // Invalid because make_shared needs to call A(int) **internally**

    std::shared_ptr<A> createNext(){ return std::shared_ptr<A>(new A(val+1)); }
    // Works fine because A(int) is called explicitly

private:

    int val;

    A(int v): val(v){}
};
Dr_Sam
la source
J'ai rencontré ce problème exact et newj'ai décidé de l'utiliser , sinon j'aurais utilisé make_shared. Voici une question connexe à ce sujet: stackoverflow.com/questions/8147027/… .
jigglypuff
6

Si vous avez besoin d'un alignement spécial de la mémoire sur l'objet 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.

Simon Ferquel
la source
2
Une deuxième situation où make_shared est inapproprié est lorsque vous souhaitez spécifier un suppresseur personnalisé.
KnowItAllWannabe
5

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

battement de glace
la source
3

Shared_ptr: Effectue deux allocations de segments

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

Make_shared: Effectue une seule allocation de segment de mémoire

  1. Contrôler les données des blocs et des objets.
James
la source
0

À propos de l'efficacité et du temps consacré à l'allocation, j'ai fait ce test simple ci-dessous, j'ai créé de nombreuses instances de ces deux façons (une à la fois):

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

    // was faster than using make_shared
    std::shared_ptr<int> foo2 = std::shared_ptr<int>(new int(10));
}

Le fait est que l'utilisation de make_shared a pris le double de temps par rapport à l'utilisation de new. Donc, en utilisant new, il y a deux allocations de tas au lieu d'une utilisant make_shared. C'est peut-être 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 parle uniquement du temps utilisé.

Orlando
la source
4
Ce test est quelque peu inutile. Le test a-t-il été effectué dans la configuration des versions avec des optimisations avérées? De plus, tous vos articles sont libérés immédiatement, ce n'est donc pas réaliste.
Phil1970
0

Je pense que la partie sécurité exceptionnelle de la réponse de m. Mpark est toujours une préoccupation valable. lors de la création d'un shared_ptr comme ceci: shared_ptr <T> (nouveau T), le nouveau T peut réussir, tandis que l'allocation de shared_ptr du bloc de contrôle peut échouer. dans ce scénario, le T nouvellement alloué fuira, car le shared_ptr n'a aucun moyen de savoir qu'il a été créé sur place et il est sûr de le supprimer. ou est-ce que je manque quelque chose? Je ne pense pas que les règles plus strictes sur l'évaluation des paramètres de fonction aident en aucune façon ici ...

Martin Vorbrodt
la source