41 votes

Pourquoi deux pointeurs bruts vers l'objet géré sont-ils nécessaires dans l'implémentation de std::shared_ptr?

Voici une citation de la section des notes d'implémentation de cppreference de std::shared_ptr, qui mentionne qu'il existe deux pointeurs différents (comme indiqué en gras) : celui qui peut être retourné par get(), et celui qui contient les données réelles dans le bloc de contrôle.

Dans une implémentation classique, std::shared_ptr ne contient que deux pointeurs :

  1. le pointeur stocké (celui retourné par get())
  2. un pointeur vers le bloc de contrôle

Le bloc de contrôle est un objet alloué dynamiquement qui contient :

  1. soit un pointeur vers l'objet géré, soit l'objet géré lui-même
  2. le destructeur (effacé par le type)
  3. l'allocation (effacée par le type)
  4. le nombre de shared_ptrs possédant l'objet géré
  5. le nombre de weak_ptrs faisant référence à l'objet géré

Le pointeur détenu par le shared_ptr directement est celui retourné par get(), tandis que le pointeur ou l'objet détenu par le bloc de contrôle est celui qui sera supprimé lorsque le nombre de propriétaires partagés atteint zéro. Ces pointeurs ne sont pas nécessairement égaux.

Ma question est, pourquoi deux pointeurs différents (les deux en gras) sont-ils nécessaires pour l'objet géré (en plus du pointeur vers le bloc de contrôle) ? Le pointeur retourné par get() ne suffit-il pas ? Et pourquoi ces pointeurs ne sont-ils pas nécessairement égaux ?

0 votes

IIUC cela implique qu'il existe en réalité, potentiellement au moins, trois pointeurs impliqués: 1. Quelque chose retourné par get(); 2. Potentiellement un pointeur à l'intérieur du bloc de contrôle vers l'objet qui sera éventuellement supprimé; 3. Un pointeur vers le bloc de contrôle. Deux d'entre eux sont détenus par le ptr partagé proprement dit; le troisième réside à l'intérieur du bloc de contrôle.

0 votes

Ceci est une question différente, comme je l'ai souligné dans mon commentaire précédent. Il ne concerne pas le bloc de contrôle par rapport à l'objet mais le fait qu'il y a deux pointeurs vers "l'objet" qui peuvent même être différents (ou il n'y aurait pas besoin d'en avoir deux). De plus le pointeur vers le bloc de contrôle. (Et la réponse semble avoir à voir avec le constructeur shared_ptr d'aliasing).

0 votes

De ce que je comprends, la réponse sur stackoverflow.com/a/26351926 concerne la différence entre les deux méthodes de construction, mais n'explique toujours pas pourquoi deux allocations potentiellement différentes sont nécessaires en premier lieu.

53voto

Angew Points 53063

La raison pour cela est que vous pouvez avoir un shared_ptr qui pointe vers quelque chose d'autre que ce qu'il possède, et c'est par conception. Cela est implémenté en utilisant le constructeur répertorié comme n° 8 sur cppreference:

template< class Y >
shared_ptr( const shared_ptr& r, T *ptr );

Un shared_ptr créé avec ce constructeur partage la propriété avec r, mais pointe vers ptr. Considérez ce code (contrived, mais illustratif) :

std::shared_ptr creator()
{
  using Pair = std::pair;

  std::shared_ptr p(new Pair(42, 3.14));
  std::shared_ptr q(p, &(p->first));
  return q;
}

Une fois que cette fonction se termine, seul un pointeur vers le sous-objet int du pair est disponible pour le code client. Mais en raison de la propriété partagée entre q et p, le pointeur q maintient l'intégralité de l'objet Pair en vie.

Une fois que la désallocation doit se produire, le pointeur vers l'intégralité de l'objet Pair doit être passé au destructeur. Par conséquent, le pointeur vers l'objet Pair doit être stocké quelque part aux côtés du destructeur—autrement dit, dans le bloc de contrôle.

Pour un exemple moins artificiel (probablement même plus proche de la motivation originale de la fonctionnalité), considérez le cas de pointer vers une classe de base. Quelque chose comme ceci:

struct Base1
{
  // :::
};

struct Base2
{
  // :::
};

struct Derived : Base1, Base2
{
 // :::
};

std::shared_ptr creator()
{
  std::shared_ptr p(new Derived());
  std::shared_ptr q(p, static_cast(p.get()));
  return q;
}

Bien sûr, la véritable implémentation de std::shared_ptr possède toutes les conversions implicites en place de sorte que la danse de p-et-q dans creator n'est pas nécessaire, mais je l'ai gardée là pour ressembler au premier exemple.

6 votes

La conversion des classes dérivées en classes de base est moins forcée comme exemple.

13 votes

Je pense que l'exemple est assez clair tel qu'il est. L'essentiel est que nous pointons vers une partie de quelque chose qui est gérée comme une entité atomique, et une paire est probablement l'exemple le plus trivial et non ambigu. L'héritage ici n'est qu'une forme spéciale de "faire partie de quelque chose".

1 votes

@Angew: Pas fw de toi. Fw de C++. Le fait que ceci est à peu près le meilleur exemple possible à former ne fait que soutenir mon argument!

1voto

qinggniq Points 21

Lien supplémentaire vers la réponse de @Angew :

Peter Dimov, Beman Dawes et Greg Colvin ont proposé shared_ptr et weak_ptr pour inclusion dans la bibliothèque standard via le premier rapport technique de la bibliothèque (connu sous le nom de TR1). La proposition a été acceptée et est finalement devenue une partie de la norme C++ dans son itération de 2011.

historique des pointeurs intelligents boost

Dans cette proposition, les auteurs ont souligné l'utilisation du "Partage de pointeur aliasing" :

Les utilisateurs avancés ont souvent besoin de la capacité de créer une instance shared_ptr p qui partage la propriété avec un autre shared_ptr (maître) q mais pointe vers un objet qui n'est pas une base de *q. *p peut être un membre ou un élément de *q, par exemple. Cette section propose un constructeur supplémentaire qui peut être utilisé à cette fin.

ils ajoutent donc un pointeur supplémentaire dans le bloc de contrôle.

0voto

Cort Ammon Points 1584

Un besoin incontournable pour un bloc de contrôle est de prendre en charge les pointeurs faibles. Il n'est pas toujours possible de notifier tous les pointeurs faibles lors de la destruction d'un objet (en fait, c'est presque toujours irréalisable). Par conséquent, les pointeurs faibles ont besoin de quelque chose sur quoi pointer jusqu'à ce qu'ils aient tous disparu. Ainsi, un bloc de mémoire doit rester en place. Ce bloc de mémoire est le bloc de contrôle. Parfois, ils peuvent être alloués ensemble, mais les allouer séparément vous permet de récupérer un objet potentiellement coûteux tout en conservant le bloc de contrôle bon marché.

La règle générale est que le bloc de contrôle persiste tant qu'il existe un seul pointeur partagé ou un pointeur faible s'y référant, tandis que l'objet peut être récupéré dès qu'il n'y a plus de pointeurs partagés qui y pointent.

Cela permet également des cas où l'objet est amené en copropriété partagée après son allocation. make_shared peut être capable de regrouper ces deux concepts en un seul bloc de mémoire, mais shared_ptr(new T) doit d'abord allouer T, puis trouver comment le partager après le fait. Lorsque cela est indésirable, boost a un concept connexe de intrusive_ptr qui effectue son comptage de référence directement à l'intérieur de l'objet plutôt qu'avec un bloc de contrôle (vous devez écrire vous-même les opérateurs d'incrémentation et de décrémentation pour que cela fonctionne).

J'ai vu des implémentations de pointeur partagé qui n'ont pas de bloc de contrôle. Au lieu de cela, les pointeurs partagés développent une liste chaînée entre eux. Tant que la liste chaînée contient 1 ou plusieurs shared_ptrs, l'objet est toujours en vie. Cependant, cette approche est plus compliquée dans un scénario multithread car vous devez maintenir la liste chaînée plutôt qu'un simple compte de référence. Son temps d'exécution est également susceptible d'être pire dans de nombreux scénarios où vous attribuez et réattribuez des shared_ptrs en raison de la lourdeur de la liste chaînée.

Il est également possible pour une implémentation haute performance d'allouer en pool les blocs de contrôle, réduisant ainsi le coût de leur utilisation à presque zéro.

1 votes

La question est "Pourquoi le bloc de contrôle contient-il une copie du pointeur stocké", et non "Pourquoi y a-t-il un bloc de contrôle ?"

-1voto

doron Points 10296

Regardons un std::shared_ptr Il s'agit d'un pointeur intelligent avec comptage de références vers un int*. Maintenant, l'objet int* ne contient aucune information de comptage de références et l'objet shared_ptr lui-même ne peut pas contenir l'information de comptage de références car il peut être détruit bien avant que le comptage de références ne tombe à zéro.

Cela signifie que nous devons avoir un objet intermédiaire pour contenir les informations de contrôle qui resteront garanties jusqu'à ce que le comptage de références tombe à zéro.

Cela dit, si vous créez un shared_ptr avec make_shared, à la fois l'objet int et le bloc de contrôle seront créés dans une mémoire contiguë, rendant le déréférencement beaucoup plus efficace.

1 votes

Vous n'avez pas répondu à la question car selon votre explication, seul un pointeur vers le bloc de contrôle est suffisant, et get() pourrait simplement renvoyer un pointeur vers l'objet réel à l'intérieur du bloc de contrôle.

3 votes

Tel que je comprends la question (et j'ai essayé de la clarifier), la question n'est pas pourquoi il y a "un pointeur vers le bloc de contrôle" et "un pointeur vers l'objet." La question est pourquoi il y a "un pointeur vers le bloc de contrôle", puis "un pointeur vers l'objet, stocké à l'intérieur du shared_ptr", et ensuite "un pointeur vers l'objet, stocké à l'intérieur du bloc de contrôle."

0 votes

@La question de @Angew est pourquoi y a-t-il DEUX pointeurs bruts alors qu'un seul est suffisant

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X