web-dev-qa-db-fra.com

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

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

Il existe de nombreux articles sur Google et stackoverflow à ce sujet, mais je ne comprends pas pourquoi make_shared est plus efficace que d'utiliser directement 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 sois en mesure de comprendre l'efficacité de make_shared. J'ai donné un exemple ci-dessus pour référence.

214
Anup Buchke

La différence est que std::make_shared effectue une allocation de segment de mémoire, alors que l'appel du constructeur std::shared_ptr en effectue deux.

Où se produisent les allocations de tas?

std::shared_ptr gère deux entités:

  • le bloc de contrôle (stocke les métadonnées telles que le nombre de références, le type effacé, etc.)
  • l'objet en cours de gestion

std::make_shared effectue une seule comptabilisation d'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 segment de mémoire pour les données gérées et le constructeur std::shared_ptr en effectue un autre pour le bloc de contrôle.

Pour plus d'informations, consultez les notes d'implémentation sur cppreference .

Mise à jour I: Exception-Safety

NOTE (2019/08/30) : Cela ne pose pas de problème depuis C++ 17, en raison des modifications apportées à l'ordre d'évaluation des arguments de la fonction. Plus précisément, chaque argument d'une fonction doit être complètement exécuté avant l'évaluation des autres arguments.

Puisque le PO semble s'interroger sur le côté sécurité des exceptions, j'ai mis à jour ma réponse.

Considérons 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")));

C++ permettant l’évaluation arbitraire des sous-expressions dans C++, vous pouvez par exemple:

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

Supposons maintenant qu'une exception soit générée à l'étape 2 (par exemple, une exception de mémoire insuffisante, le constructeur Rhs a lancé une exception). Nous perdons ensuite la mémoire allouée à l’étape 1, car rien n’aura eu la chance de la nettoyer. Le problème réside dans le fait que le pointeur brut n'a pas été immédiatement transmis au constructeur std::shared_ptr.

Une façon de résoudre ce problème consiste à les faire sur des lignes séparées afin que cette commande 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);

Le moyen préféré pour résoudre ce problème est bien sûr d'utiliser std::make_shared.

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 :

Comme il n'y a qu'une seule allocation, la mémoire de la pointee ne peut pas être désallouée tant que le bloc de contrôle n'est plus utilisé. Un weak_ptr peut 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). Pour ce faire, ils vérifient le nombre de shared_ptrs possédant l'objet géré, qui est stocké dans le bloc de contrôle. Le résultat est que les blocs de contrôle sont actifs jusqu'au compte shared_ptr et au weak_ptr comptés tous les deux au 0.

Retour à std::make_shared

Étant donné que std::make_shared effectue une seule allocation de segment de mémoire à la fois pour le bloc de contrôle et l'objet géré, il n'existe aucun moyen de libérer la mémoire du bloc de contrôle et l'objet géré indépendamment. Nous devons attendre jusqu'à ce que nous puissions libérer le bloc de contrôle et l'objet géré, ce qui arrive jusqu'à ce qu'il n'y ait plus de shared_ptrs ou weak_ptrs en vie.

Supposons que nous effectuions plutôt deux allocations de tas pour le bloc de contrôle et l'objet géré via le constructeur new et shared_ptr. 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 actif, et la mémoire du bloc de contrôle (peut-être plus tard) lorsqu'il n'y a pas de weak_ptrs vivant.

300
mpark

Le pointeur partagé gère à la fois l'objet lui-même et un petit objet contenant le décompte de références et d'autres données de gestion. make_shared peut allouer un seul bloc de mémoire pour contenir les deux; La construction d'un pointeur partagé entre un pointeur et un objet déjà alloué devra allouer un deuxième 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 du tout besoin de gérer les pointeurs new et raw, ce qui améliore la sécurité des exceptions - il n'est pas possible de générer une exception après avoir alloué l'objet mais avant de l'attribuer au pointeur intelligent. .

19
Mike Seymour

Il existe 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 y accéder, alors que la variante avec les nouvelles œuvres convient .

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){}
};
13
Dr_Sam

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.

3
Simon Ferquel

Shared_ptr: effectue deux allocations de tas

  1. Bloc de contrôle (nombre de références)
  2. Objet en cours de gestion

Make_shared: n'effectue qu'une seule allocation de tas

  1. Bloc de contrôle et données d'objet.
2
James

En ce qui concerne l’efficacité et le temps consacré à l’allocation, j’ai fait ce test simple ci-dessous, j’ai créé plusieurs 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<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 deux fois plus de temps que l’utilisation de new. Donc, en utilisant new, il y a deux allocations de tas au lieu d'une avec make_shared. C’est peut-être un test stupide, mais cela ne montre-t-il pas qu’utiliser make_shared prend plus de temps que d’utiliser new? Bien sûr, je parle seulement du temps utilisé.

0
orlando

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

0
icebeat