c++ shared_ptr - Quand std::weak_ptr est-il utile?




unique_ptr (9)

http://en.cppreference.com/w/cpp/memory/weak_ptr std :: weak_ptr est un pointeur intelligent qui contient une référence non-propriétaire ("faible") à un objet géré par std :: shared_ptr. Il doit être converti en std :: shared_ptr afin d'accéder à l'objet référencé.

Modèles std :: weak_ptr propriété temporaire: quand un objet a besoin d'être accédé seulement s'il existe, et il peut être supprimé à tout moment par quelqu'un d'autre, std :: weak_ptr est utilisé pour suivre l'objet, et il est converti en std: : shared_ptr pour assumer la propriété temporaire. Si le fichier original std :: shared_ptr est détruit à ce moment, la durée de vie de l'objet est prolongée jusqu'à ce que le fichier temporaire std :: shared_ptr soit également détruit.

De plus, std :: weak_ptr est utilisé pour casser les références circulaires de std :: shared_ptr.

J'ai commencé à étudier les pointeurs intelligents de C ++ 11 et je ne vois pas l'utilité de std::weak_ptr . Quelqu'un peut-il me dire quand std::weak_ptr est utile / nécessaire?


Ils sont utiles avec Boost.Asio lorsque vous n'êtes pas certain qu'un objet cible existe toujours lorsqu'un gestionnaire asynchrone est appelé. L'astuce consiste à lier un weak_ptr dans l'objet gestionnaire asynchonous, en utilisant des captures std::bind ou lambda.

void MyClass::startTimer()
{
    std::weak_ptr<MyClass> weak = shared_from_this();
    timer_.async_wait( [weak, this](const boost::system::error_code& ec)
    {
        auto self = weak.lock();
        if (self)
        {
            self->handleTimeout();
        }
        else
        {
            std::cout << "Target object no longer exists!\n";
        }
    } );
}

Ceci est une variante de l'idiome self = shared_from_this() souvent vu dans les exemples Boost.Asio, où un gestionnaire asynchrone en attente ne prolongera pas la durée de vie de l'objet cible, mais est toujours sûr si l'objet cible est supprimé.


Lorsque vous utilisez des pointeurs, il est important de comprendre les différents types de pointeurs disponibles et quand il est judicieux d'utiliser chacun d'eux. Il existe quatre types de pointeurs dans deux catégories:

  • Pointeurs Raw:
    • Pointeur brut [c.-à-d. SomeClass* ptrToSomeClass = new SomeClass(); ]
  • Pointeurs intelligents:
    • Pointeurs uniques [ie std::unique_ptr<SomeClass> uniquePtrToSomeClass ( new SomeClass() ); ]
    • Pointeurs partagés [c'est-à-dire std::shared_ptr<SomeClass> sharedPtrToSomeClass ( new SomeClass() ); ]
    • Pointeurs faibles [c'est-à-dire std::weak_ptr<SomeClass> weakPtrToSomeWeakOrSharedPtr ( weakOrSharedPtr ); ]

Les pointeurs bruts (parfois appelés «pointeurs hérités» ou «pointeurs C») fournissent un comportement de pointeur «bare-bones» et sont une source courante de bogues et de fuites de mémoire. Les pointeurs bruts ne fournissent aucun moyen de garder la trace de la propriété de la ressource et les développeurs doivent appeler 'delete' manuellement pour s'assurer qu'ils ne créent pas de fuite de mémoire. Cela devient difficile si la ressource est partagée car il peut être difficile de savoir si des objets pointent toujours vers la ressource. Pour ces raisons, les pointeurs bruts devraient généralement être évités et utilisés uniquement dans les sections critiques du code ayant une portée limitée.

Les pointeurs uniques sont un pointeur intelligent de base qui «possède» le pointeur brut sous-jacent à la ressource et qui est chargé d'appeler supprimer et libérer la mémoire allouée une fois que l'objet qui possède le pointeur unique est hors de portée. Le nom "unique" fait référence au fait qu'un seul objet peut "posséder" le pointeur unique à un moment donné. La propriété peut être transférée à un autre objet via la commande move, mais un pointeur unique ne peut jamais être copié ou partagé. Pour ces raisons, les pointeurs uniques sont une bonne alternative aux pointeurs bruts dans le cas où un seul objet a besoin du pointeur à un moment donné, ce qui évite au développeur de libérer de la mémoire à la fin du cycle de vie de l'objet propriétaire.

Les pointeurs partagés sont un autre type de pointeur intelligent qui sont similaires aux pointeurs uniques, mais qui permettent à de nombreux objets d'appartenir au pointeur partagé. Comme pointeur unique, les pointeurs partagés sont responsables de la libération de la mémoire allouée une fois que tous les objets sont pointés vers la ressource. Il accomplit ceci avec une technique appelée le comptage de référence. Chaque fois qu'un nouvel objet prend possession du pointeur partagé, le nombre de références est incrémenté de un. De même, lorsqu'un objet est hors de portée ou arrête de pointer sur la ressource, le nombre de références est décrémenté de un. Lorsque le compte de référence atteint zéro, la mémoire allouée est libérée. Pour ces raisons, les pointeurs partagés sont un type très puissant de pointeur intelligent qui doit être utilisé chaque fois que plusieurs objets doivent pointer vers la même ressource.

Enfin, les pointeurs faibles sont un autre type de pointeur intelligent qui, plutôt que de pointer directement sur une ressource, pointent vers un autre pointeur (faible ou partagé). Les pointeurs faibles ne peuvent pas accéder directement à un objet, mais ils peuvent déterminer si l'objet existe toujours ou s'il a expiré. Un pointeur faible peut être temporairement converti en un pointeur intelligent pour accéder à l'objet pointé (à condition qu'il existe toujours). Pour illustrer, considérons l'exemple suivant:

  • Vous êtes occupé et avez des réunions qui se chevauchent: Réunion A et Réunion B
  • Vous décidez d'aller à la réunion A et votre collègue va à la réunion B
  • Vous dites à votre collègue que si la réunion B se poursuit après la fin de la réunion A, vous rejoindrez
  • Les deux scénarios suivants pourraient jouer:
    • La réunion A se termine et la réunion B continue, vous vous inscrivez donc
    • La réunion A se termine et la réunion B est également terminée, vous ne participez donc pas

Dans l'exemple, vous avez un pointeur faible vers la réunion B. Vous n'êtes pas un "propriétaire" dans la réunion B, de sorte qu'il peut se terminer sans vous, et vous ne savez pas s'il s'est terminé ou non, sauf si vous vérifiez. Si ce n'est pas terminé, vous pouvez vous inscrire et participer, sinon, vous ne pouvez pas. Ceci est différent d'avoir un pointeur partagé vers la réunion B parce que vous seriez alors un «propriétaire» dans les deux réunions A et B (participant aux deux en même temps).

L'exemple illustre comment un pointeur faible fonctionne et est utile quand un objet doit être un observateur extérieur, mais ne veut pas la responsabilité de la propriété. Ceci est particulièrement utile dans le cas où deux objets doivent se pointer l'un vers l'autre (une référence circulaire). Avec les pointeurs partagés, aucun objet ne peut être libéré car ils sont toujours "fortement" pointés par l'autre objet. Avec des pointeurs faibles, les objets peuvent être accédés en cas de besoin, et libérés quand ils n'ont plus besoin d'exister.


Voici un exemple, donné par @jleahy: Supposons que vous ayez une collection de tâches, exécutées de manière asynchrone, et gérées par un std::shared_ptr<Task> . Vous voudrez peut-être faire quelque chose avec ces tâches périodiquement, donc un événement timer peut traverser un std::vector<std::weak_ptr<Task>> et donner quelque chose aux tâches. Cependant, une tâche peut simultanément avoir décidé qu'elle n'est plus nécessaire et mourir. Le minuteur peut ainsi vérifier si la tâche est toujours active en créant un pointeur partagé à partir du pointeur faible et en utilisant ce pointeur partagé, à condition qu'il ne soit pas nul.


shared_ptr : contient l'objet réel.

weak_ptr : utilise le lock pour se connecter au propriétaire réel ou renvoie NULL dans le cas contraire.

Grosso modo, le rôle de weak_ptr est similaire au rôle de l' agence de logement . Sans agents, pour obtenir une maison en loyer, nous devrons peut-être vérifier les maisons au hasard dans la ville. Les agents veillent à ce que nous ne visitons que les maisons qui sont encore accessibles et disponibles à la location.


std::weak_ptr est un très bon moyen de résoudre le problème du pointeur qui pend . En utilisant simplement des pointeurs bruts, il est impossible de savoir si les données référencées ont été désallouées ou non. Au lieu de cela, en laissant std::shared_ptr gérer les données et en fournissant std::weak_ptr aux utilisateurs des données, les utilisateurs peuvent vérifier la validité des données en appelant expired() ou lock() .

Vous ne pouvez pas le faire avec std::shared_ptr seul, car toutes les instances de std::shared_ptr partagent la propriété des données qui ne sont pas supprimées avant que toutes les instances de std::shared_ptr soient supprimées. Voici un exemple de vérification du pointeur dangling à l'aide de lock() :

#include <iostream>
#include <memory>

int main()
{
    // OLD, problem with dangling pointer
    // PROBLEM: ref will point to undefined data!

    int* ptr = new int(10);
    int* ref = ptr;
    delete ptr;

    // NEW
    // SOLUTION: check expired() or lock() to determine if pointer is valid

    // empty definition
    std::shared_ptr<int> sptr;

    // takes ownership of pointer
    sptr.reset(new int);
    *sptr = 10;

    // get pointer to data without taking ownership
    std::weak_ptr<int> weak1 = sptr;

    // deletes managed object, acquires new pointer
    sptr.reset(new int);
    *sptr = 5;

    // get pointer to new data without taking ownership
    std::weak_ptr<int> weak2 = sptr;

    // weak1 is expired!
    if(auto tmp = weak1.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak1 is expired\n";

    // weak2 points to new data (5)
    if(auto tmp = weak2.lock())
        std::cout << *tmp << '\n';
    else
        std::cout << "weak2 is expired\n";
}

Un bon exemple serait un cache.

Pour les objets auxquels vous avez récemment accédé, vous voulez les garder en mémoire, vous devez donc leur indiquer un pointeur fort. Périodiquement, vous analysez le cache et décidez quels objets n'ont pas été accédés récemment. Vous n'avez pas besoin de garder ceux en mémoire, alors vous vous débarrassez du pointeur fort.

Mais que se passe-t-il si cet objet est utilisé et un autre code contient un pointeur fort? Si le cache se débarrasse de son seul pointeur sur l'objet, il ne peut plus le retrouver. Le cache garde donc un pointeur faible vers les objets qu'il doit trouver s'il reste en mémoire.

C'est exactement ce que fait un pointeur faible - il vous permet de localiser un objet s'il est toujours là, mais ne le garde pas si rien d'autre n'en a besoin.


Une autre réponse, espérons-le plus simple. (pour les autres googleurs)

Supposons que vous ayez Member objets Team et Member .

Évidemment, c'est une relation: l'objet Team aura des pointeurs vers ses Members . Et il est probable que les membres auront également un pointeur arrière vers leur objet Team .

Ensuite, vous avez un cycle de dépendance. Si vous utilisez shared_ptr , les objets ne seront plus automatiquement libérés lorsque vous abandonnerez la référence, car ils se référenceront de manière cyclique. C'est une fuite de mémoire.

Vous cassez ceci en utilisant weak_ptr . Le "propriétaire" utilise généralement shared_ptr et le "owned" utilise un weak_ptr à son parent, et le convertit temporairement en shared_ptr lorsqu'il a besoin d'accéder à son parent.

Stocke un faible ptr:

weak_ptr<Parent> parentWeakPtr_ = parentSharedPtr; // automatic conversion to weak from shared

alors utilisez-le au besoin

shared_ptr<Parent> tempParentSharedPtr = parentWeakPtr_.lock(); // on the stack, from the weak ptr
if( not tempParentSharedPtr ) {
  // yes it may failed if parent was freed since we stored weak_ptr
} else {
  // do stuff
}
// tempParentSharedPtr is released when it goes out of scope

Je peux reproduire vos résultats sur ma machine avec les options que vous écrivez dans votre message.

Cependant, si -flto aussi l' optimisation du temps de liaison (je passe aussi l'indicateur -flto à gcc 4.7.2), les résultats sont identiques:

(Je compile votre code original, avec container.push_back(Item()); )

$ g++ -std=c++11 -O3 -flto regr.cpp && perf stat -r 10 ./a.out 

 Performance counter stats for './a.out' (10 runs):

         35.426793 task-clock                #    0.986 CPUs utilized            ( +-  1.75% )
                 4 context-switches          #    0.116 K/sec                    ( +-  5.69% )
                 0 CPU-migrations            #    0.006 K/sec                    ( +- 66.67% )
            19,801 page-faults               #    0.559 M/sec                  
        99,028,466 cycles                    #    2.795 GHz                      ( +-  1.89% ) [77.53%]
        50,721,061 stalled-cycles-frontend   #   51.22% frontend cycles idle     ( +-  3.74% ) [79.47%]
        25,585,331 stalled-cycles-backend    #   25.84% backend  cycles idle     ( +-  4.90% ) [73.07%]
       141,947,224 instructions              #    1.43  insns per cycle        
                                             #    0.36  stalled cycles per insn  ( +-  0.52% ) [88.72%]
        37,697,368 branches                  # 1064.092 M/sec                    ( +-  0.52% ) [88.75%]
            26,700 branch-misses             #    0.07% of all branches          ( +-  3.91% ) [83.64%]

       0.035943226 seconds time elapsed                                          ( +-  1.79% )



$ g++ -std=c++98 -O3 -flto regr.cpp && perf stat -r 10 ./a.out 

 Performance counter stats for './a.out' (10 runs):

         35.510495 task-clock                #    0.988 CPUs utilized            ( +-  2.54% )
                 4 context-switches          #    0.101 K/sec                    ( +-  7.41% )
                 0 CPU-migrations            #    0.003 K/sec                    ( +-100.00% )
            19,801 page-faults               #    0.558 M/sec                    ( +-  0.00% )
        98,463,570 cycles                    #    2.773 GHz                      ( +-  1.09% ) [77.71%]
        50,079,978 stalled-cycles-frontend   #   50.86% frontend cycles idle     ( +-  2.20% ) [79.41%]
        26,270,699 stalled-cycles-backend    #   26.68% backend  cycles idle     ( +-  8.91% ) [74.43%]
       141,427,211 instructions              #    1.44  insns per cycle        
                                             #    0.35  stalled cycles per insn  ( +-  0.23% ) [87.66%]
        37,366,375 branches                  # 1052.263 M/sec                    ( +-  0.48% ) [88.61%]
            26,621 branch-misses             #    0.07% of all branches          ( +-  5.28% ) [83.26%]

       0.035953916 seconds time elapsed  

Pour les raisons, il faut regarder le code d'assemblage généré ( g++ -std=c++11 -O3 -S regr.cpp ). En mode C ++ 11, le code généré est significativement plus encombré que pour le mode C ++ 98 et en soulignant la fonction
void std::vector<Item,std::allocator<Item>>::_M_emplace_back_aux<Item>(Item&&)
échoue en mode C ++ 11 avec la inline-limit par défaut.

Cette erreur en ligne a un effet domino. Non parce que cette fonction est appelée (elle n'est même pas appelée!) Mais parce que nous devons être préparés: Si elle est appelée, la fonction argments ( Item.a et Item.b ) doit déjà être au bon endroit. Cela conduit à un code assez désordonné.

Voici la partie pertinente du code généré pour le cas où inlining réussit :

.L42:
    testq   %rbx, %rbx  # container$D13376$_M_impl$_M_finish
    je  .L3 #,
    movl    $0, (%rbx)  #, container$D13376$_M_impl$_M_finish_136->a
    movl    $0, 4(%rbx) #, container$D13376$_M_impl$_M_finish_136->b
.L3:
    addq    $8, %rbx    #, container$D13376$_M_impl$_M_finish
    subq    $1, %rbp    #, ivtmp.106
    je  .L41    #,
.L14:
    cmpq    %rbx, %rdx  # container$D13376$_M_impl$_M_finish, container$D13376$_M_impl$_M_end_of_storage
    jne .L42    #,

C'est une belle et compacte boucle. Maintenant, comparons cela à celui du cas inline en échec :

.L49:
    testq   %rax, %rax  # D.15772
    je  .L26    #,
    movq    16(%rsp), %rdx  # D.13379, D.13379
    movq    %rdx, (%rax)    # D.13379, *D.15772_60
.L26:
    addq    $8, %rax    #, tmp75
    subq    $1, %rbx    #, ivtmp.117
    movq    %rax, 40(%rsp)  # tmp75, container.D.13376._M_impl._M_finish
    je  .L48    #,
.L28:
    movq    40(%rsp), %rax  # container.D.13376._M_impl._M_finish, D.15772
    cmpq    48(%rsp), %rax  # container.D.13376._M_impl._M_end_of_storage, D.15772
    movl    $0, 16(%rsp)    #, D.13379.a
    movl    $0, 20(%rsp)    #, D.13379.b
    jne .L49    #,
    leaq    16(%rsp), %rsi  #,
    leaq    32(%rsp), %rdi  #,
    call    _ZNSt6vectorI4ItemSaIS0_EE19_M_emplace_back_auxIIS0_EEEvDpOT_   #

Ce code est encombré et il se passe beaucoup plus de choses dans la boucle que dans le cas précédent. Avant l' call la fonction (dernière ligne affichée), les arguments doivent être placés de manière appropriée:

leaq    16(%rsp), %rsi  #,
leaq    32(%rsp), %rdi  #,
call    _ZNSt6vectorI4ItemSaIS0_EE19_M_emplace_back_auxIIS0_EEEvDpOT_   #

Même si cela n'est jamais réellement exécuté, la boucle arrange les choses avant:

movl    $0, 16(%rsp)    #, D.13379.a
movl    $0, 20(%rsp)    #, D.13379.b

Cela conduit au code désordonné. S'il n'y a pas d' call fonction, car inlining réussit, nous n'avons que 2 instructions de déplacement dans la boucle et il n'y a pas de %rsp avec le %rsp (pointeur de la pile). Cependant, si l'inline échoue, nous obtenons 6 coups et nous %rsp beaucoup avec le %rsp .

Juste pour justifier ma théorie (notez la -finline-limit ), les deux en mode C ++ 11:

 $ g++ -std=c++11 -O3 -finline-limit=105 regr.cpp && perf stat -r 10 ./a.out

 Performance counter stats for './a.out' (10 runs):

         84.739057 task-clock                #    0.993 CPUs utilized            ( +-  1.34% )
                 8 context-switches          #    0.096 K/sec                    ( +-  2.22% )
                 1 CPU-migrations            #    0.009 K/sec                    ( +- 64.01% )
            19,801 page-faults               #    0.234 M/sec                  
       266,809,312 cycles                    #    3.149 GHz                      ( +-  0.58% ) [81.20%]
       206,804,948 stalled-cycles-frontend   #   77.51% frontend cycles idle     ( +-  0.91% ) [81.25%]
       129,078,683 stalled-cycles-backend    #   48.38% backend  cycles idle     ( +-  1.37% ) [69.49%]
       183,130,306 instructions              #    0.69  insns per cycle        
                                             #    1.13  stalled cycles per insn  ( +-  0.85% ) [85.35%]
        38,759,720 branches                  #  457.401 M/sec                    ( +-  0.29% ) [85.43%]
            24,527 branch-misses             #    0.06% of all branches          ( +-  2.66% ) [83.52%]

       0.085359326 seconds time elapsed                                          ( +-  1.31% )

 $ g++ -std=c++11 -O3 -finline-limit=106 regr.cpp && perf stat -r 10 ./a.out

 Performance counter stats for './a.out' (10 runs):

         37.790325 task-clock                #    0.990 CPUs utilized            ( +-  2.06% )
                 4 context-switches          #    0.098 K/sec                    ( +-  5.77% )
                 0 CPU-migrations            #    0.011 K/sec                    ( +- 55.28% )
            19,801 page-faults               #    0.524 M/sec                  
       104,699,973 cycles                    #    2.771 GHz                      ( +-  2.04% ) [78.91%]
        58,023,151 stalled-cycles-frontend   #   55.42% frontend cycles idle     ( +-  4.03% ) [78.88%]
        30,572,036 stalled-cycles-backend    #   29.20% backend  cycles idle     ( +-  5.31% ) [71.40%]
       140,669,773 instructions              #    1.34  insns per cycle        
                                             #    0.41  stalled cycles per insn  ( +-  1.40% ) [88.14%]
        38,117,067 branches                  # 1008.646 M/sec                    ( +-  0.65% ) [89.38%]
            27,519 branch-misses             #    0.07% of all branches          ( +-  4.01% ) [86.16%]

       0.038187580 seconds time elapsed                                          ( +-  2.05% )

En effet, si nous demandons au compilateur d'essayer un peu plus difficile d'intégrer cette fonction, la différence de performance disparaît.

Alors, quelle est la portée de cette histoire? Ces échecs en ligne peuvent vous coûter beaucoup et vous devriez utiliser pleinement les capacités du compilateur: je ne peux que recommander l'optimisation du temps de liaison. Cela a donné un coup de fouet significatif à mes programmes (jusqu'à 2,5x) et tout ce que je devais faire était de passer le drapeau -flto . C'est une bonne affaire! ;)

Cependant, je ne recommande pas de balayer votre code avec le mot-clé inline; Laissez le compilateur décider quoi faire. (L'optimiseur est autorisé à traiter le mot-clé inline comme un espace blanc de toute façon.)

Bonne question, +1!





c++ c++11 shared-ptr smart-pointers weak-ptr