|
Publicité ' | ||||||||||||||||||||||||
|
|
#1 |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Bonjour.
Encore un petit problème de conception... Je dispose d'objets (polymorphiques ou non) à sémantique d'entité, utilisés un peu partout dans le programme. Ces objets sont lus à partir d'un fichier, et associés à une clé pour pouvoir les récupérer facilement. Jusqu'ici, ils étaient stockés dans des conteneurs, et utilisés dans le programme à l'aide de pointeurs. Ils étaient créés par une simili-fabrique, et détruits à la fin du programme en vidant les conteneurs. Histoire d'améliorer un peu tout ça, je compte utiliser des pointeurs intelligents afin de les détruire automatiquement (et faire de vraies fabriques). Logiquement, je suis parti sur quelque chose qui conserve un peu la structure : les objets sont stockée dans le conteneurs via des std::unique_ptr, et on les utilises dans le programme via des références/std::reference_wrapper. Mais après tout, même si ces objets sont « unique », ils sont « partagés » entre plusieurs modules du programme. std::shared_ptr serait donc peut-être plus cohérent. Alors on stocke via des std::shared_ptr, et on utilise via des std::weak_ptr. À la réflexion, ce n'est peut-être pas utile d'avoir un seul std::shared_ptr par objet. Et puis comme ils sont « partagés », il n'y a pas vraiment de raison de les stocker à un endroit particulier. De plus, si un objet n'est plus utilisé, il sera directement détruit, et non plus stocké pour rien. Mais du coup, dans mes tables, à quoi associer les clés ? Pour que ce soit efficace, il faut créer un nouveau std::shared_ptr à partir d'un std::shared_ptr existant. Donc au final, je serai tout de même obligé de stocker mes objets dans des conteneurs... Bref, voici le déroulement de mon raisonnement, pour arriver la question du titre :
Je me doute bien que vous ne pourrez pas me répondre sans avoir des informations précises sur mon projet. Alors ce que j'aimerais surtout savoir, c'est à quoi je dois réfléchir et ce que je dois prendre en compte pour faire mon choix. Voilà. Merci d'avance.
__________________
Steph |
|
|
00
|
|
|
#2 |
|
Membre Expert
![]() ![]() |
Il me semble que Herb Sutter, Andrei Alexandrescu et Scott Meyer sont d'accord sur la logique suivante :
Si l'objet à un détenteur (qui a le droit de vie et de mort) unique, alors utiliser std::unique_ptr. Si cet objet doit être référencé (membre) par d'autres objets, alors utiliser un pointeur "nu" est une bonne idée. Si on le passe juste à des fonctions, une référence est supérieure, évidemment. Si l'objet doit vivre tant qu'il est utilisé (droit de vie et de mort partagé) alors utiliser std::shared_ptr. Pourquoi utiliser un pointeur nul dans le cas ou on utilise std::unique_ptr? Parceque 1. c'est simple 2. si on suit ces règles, on se retrouve quasimment avec uniquement des std::unique_ptr, std::shared_ptr et des pointeurs nus, et du coup il est facile de les interpréter : un pointeur nu indique que celui qui l'a n'a pas le droit de vie et de mort sur l'objet, il ne fait que l'utiliser (en partant du principe qu'il est valide). Note toutefois que cela est valide uniquement pour la sémantique d'entité. Pour la sémantique de valeur, évidemment, rien ne vaut une bonne copie (optimizée par sémantique de mouvement si besoin). edit - conclusion : Dans ton cas, le tout est de savoir qui a le droit de vie ou de mort sur ton objet. Si il doit mourrir quand un objet spécifique le lui dit, alors std::unique_ptr sans hésiter, et utilise des références pour passer l'objet à d'autres parties du code, ou des pointeurs nus si ils doivent garder une référence au dit objet. Si tout le code qui référence l'objet doit absolument l'avoir de valide et que l'objet peut mourrir si plus personne n'en a besoin (souvent des resources partagées), alors std::shared_ptr est préférable. Personellement, je n'utilise std::weak_ptr que pour briser les cycles. |
|
|
10
|
|
|
#3 | |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Ok pour les pointeur nus.
Finalement, ils ne sont pas si mauvais que ça, si on les utilise correctement... Pour la distinction d'utilisation des pointeurs intelligents, j'étais arrivé à la même conclusion. (ouf ! Citation:
L'objet doit être valide tant qu'on a besoin de lui, et lorsque l'on a plus besoin de lui... eh bien on n'a plus besoin de lui. Qu'il soit détruit ou non à ce moment-là n'a pas vraiment d'importance, sauf si l'on a besoin de la mémoire qu'il occupe. C'est pour cette raison que j'avais pensé aux std::shared_ptr. Sauf que lorsque je veux le référencer, il faut d'abord que je le retrouve, ou que je le crée s'il n'existe pas encore. Et à ce moment-là, les éventuelles autres références à l'objet sont éparpillées dans le programme, et je n'ai aucun moyen de savoir où. Il me faut donc une table qui associe l'objet à une clé. (À moins qu'il y ait moyen de faire autrement ?) Mais comment référencer l'objet en question dans la table ? Une référence C++ ou un pointeur nu ? Le nombre de références des std::shared_ptr ne sera pas correct ; et l'objet sera détruit plusieurs fois (si le programme ne plante pas avant)... ![]() Un std::shared_ptr ? Dans ce cas il y aura toujours au moins une référence active tant que le pointeur intelligent sera dans la table. Autant donner la responsabilité à la table de détruire l'objet (et donc l'utilisation de std::unique_ptr). En fait, j'ai l'impression que conceptuellement, il me faudrait un std::shared_ptr, mais qu'en pratique, je serai obligé d'utiliser un std::unique_ptr... Sauf si j'ai une erreur dans mon raisonnement. J'en ai une ? Euh... c'est à dire ?
__________________
Steph |
|
|
|
00
|
|
|
#4 |
![]() ![]() Guillaume BelzBiochimiste Inscription : novembre 2008 Messages : 2 869 ![]() |
Utilise shared_ptr dans les classes qui utilisent tes objets (ils ont la responsabilité de la durée de vie) et un weak_ptr dans la collection (pas de responsabilité de durée de vie).
Il suffit alors de créer l'objet s'il n'est pas dans la collection ou s'il est expired(). Briser les cycles : si un objet à un shared_ptr qui a lui même un shared_ptr sur le premier, les 2 objets auront toujours au moins 1 référence valide et ne seront pas détruit. On utilise un weak_ptr pour éviter ce problème.
__________________
Vous souhaitez rejoindre l'équipe de bénévoles qui fait vivre Developpez (traduction, rédaction, modération) ? Contactez moi par MP. Ma page personnelle avec la liste de mes articles - Mon blog sur la programmation des GPU. Je suis régulièrement sur le chat pour les questions C++/Qt. |
|
10
|
|
|
#5 |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Ah bah oui, je n'y avais pas pensé...
Mais si je crée des std::shared_ptr à partir d'un std::weak_ptr, ils partageront bien le même comptage de références ? Et ok pour les cycles.
__________________
Steph |
|
|
00
|
|
|
#6 |
|
Membre Expert
![]() Jean-Bernard Inscription : mars 2007 Messages : 817 ![]() |
Ben ça ça dépend de lui. Typiquement là je bosse dans un code où c'est l'inverse, une collection a le droit de vie et de mort exclusif (donc shared_ptr où son équivalent car j'ai pas la STL sur ce projet) et j'utilise des pointeurs nus (ou des weak_ptr dans le cas où un objet a besoin de stocker cette référence) ailleurs.
|
|
|
00
|
|
|
#7 |
![]() ![]() Guillaume BelzBiochimiste Inscription : novembre 2008 Messages : 2 869 ![]() |
Oui si tu créés le shared_ptr à partir du constructeur ou de lock() (mais pas si tu t'amuses à récupérer le pointeur nu pour créer un nouveau shared_ptr).
__________________
Vous souhaitez rejoindre l'équipe de bénévoles qui fait vivre Developpez (traduction, rédaction, modération) ? Contactez moi par MP. Ma page personnelle avec la liste de mes articles - Mon blog sur la programmation des GPU. Je suis régulièrement sur le chat pour les questions C++/Qt. |
|
00
|
|
|
#8 | ||
|
Membre Expert
![]() ![]() |
Citation:
Tu dis au départ que l'objet doit toujours être valide, qu'il peut être détruit uniquement une fois non utilisé ET que la table ne le référence plus Donc ça sous entends que même si la table est détruite, ceux qui s'en servent doivent maintenir cet objet en vie. Je ne vois pas le souci, il n'y a que std::shared_ptr, autant dans la table que dans le reste du code. Citation:
Dans ce genre de cas si tu utilises des shared_ptr A,B et C seront toujours vivant parcequ'ils se referencent les uns les autres. Pour régler le problème, on fait la différence entre une référence forte (détiens, a un impacte sur la vie et la mort de) et une référence faible (référence sans détenir). D'où l'interet des std::weak_ptr. |
||
|
|
00
|
|
|
#9 | ||
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Citation:
Citation:
C'est ça qui me posait problème : si je mets un std::shared_ptr dans la table aussi, j'aurai une référence de trop (tant qu'il sera dans la table). En fait, en pratique, la table est remplie au début du programme, et on n'en retire rien jusqu'à sa destruction à la fin du programme, même si on ne l'utilise plus une fois les objets nécessaires créés. Donc soit elle est totalement responsable de la durée de vie de l'objet, et alors ailleurs on y fait référence sans avoir de responsabilité dessus, soit les bouts de code où on utilise l'objet sont responsable de sa durée de vie, et la table y fait simplement référence sans responsabilité dessus. Je pense que la seconde solution est la meilleure, car dans l'absolu, on doit pouvoir utiliser l'objet que la table soit présente ou non. Donc la solution de gbdivers, à savoir utiliser un std::weak_ptr dans la table et des std::shared_ptr me plaît bien. Merci à tous. Je passe cette discussion en , mais si vous avez encore des remarques, je les lirai et j'en tiendrai compte.
__________________
Steph |
||
|
|
00
|
|
|
#10 |
![]() ![]() Guillaume BelzBiochimiste Inscription : novembre 2008 Messages : 2 869 ![]() |
Quelques remarques supplémentaires :
- on est bien d'accord que si la table est chargée au démarrage et qu'elle est détruite lorsque l'on quitte le programme, cela veut dire que les objets ont la durée de vie du programme et dans ce cas, une collection avec unique_ptr sera mieux - l'intérêt de la collection avec weak_ptr est de gérer dynamiquement les objets. Donc de pouvoir les créer et détruire à la volée - il reste en effet le problème de weak_ptr invalide restant dans la collection. 2 solutions : * on pourrait avoir une fonction collection::delete(ptr) qui permet de supprimer un objet vide. Mais on perd l'intérêt de donner la responsabilité aux utilisateurs. * on pourrait avoir une fonction collection::update() qui permet de mettre à jour la collection pour supprimer les weak_ptr invalides et qui serait appelé lors d'un delete par un utilisateur (beaucoup d'update inutile ?) * on pourrait avoir une fonction collection::update() qui permet de mettre à jour la collection pour supprimer les weak_ptr invalides et qui serait appelé lors d'un appel à factory::create() * probablement d'autres choses De manière plus général, si les objets gérés par la collection sont peu nombreux (par exemple créés par factory::create(enum)), alors on peu se permettre de ne pas nettoyer la collection. Si par contre, la collection est grande (voir infinie, par exemple avec un factory::create(float)) alors, il faut mieux faire du ménage de temps en temps
__________________
Vous souhaitez rejoindre l'équipe de bénévoles qui fait vivre Developpez (traduction, rédaction, modération) ? Contactez moi par MP. Ma page personnelle avec la liste de mes articles - Mon blog sur la programmation des GPU. Je suis régulièrement sur le chat pour les questions C++/Qt. |
|
10
|
|
|
#11 |
|
Membre Expert
![]() ![]() |
Concernant la solution, j'avais mal compris la question et je suis d'accord avec gbdrivers.
|
|
|
00
|
|
|
#12 |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
En fait, en pratique les objets référencés ne sont détruits qu'à la fin de l'application.
Mais c'est parce qu'ils sont utilisés jusqu'au bout. Quant aux tables, une fois les objets référençants créés, elles ne servent plus, et en théorie pourraient être détruites. Sans rendre invalides les objets référencés, cela va de soi... Ceci dit, on pourrait imaginer que des évolutions futures demandent d'utiliser les tables longtemps après la création des objets référençants. Ou qu'un objet référencé ne soit plus utilisé. Recréer un objet référencé n'est pas vraiment un problème, la plupart ne sont pas très gros. Et les tables ne seront pas infinies, mais certaines peuvent tout de même contenir beaucoup d'éléments. Au final, je crois que le *vrai* problème, c'est que je n'arrive pas à bien déterminer qui a la responsabilité de détruire les objets référencés... Quoi qu'il en soit, merci de vos réponses.
__________________
Steph |
|
|
00
|
|
|
#13 |
|
Membre Expert
![]() ![]() |
Au pire, fais deux typedefs, un pour celui qui manipule ces objets et a le droit de vie et de mort, l'autre pour les "références" aux objets.
Comme ça tu peux changer au moins le nom du type facilement. |
|
|
10
|
|
|
#14 |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Bonne idée.
Merci.
__________________
Steph |
|
|
00
|
|
|
#15 |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Bon, à un moment j'ai pensé à faire en sorte que les objets référencés suppriment le weak_ptr associé de la table lorsqu'il ne sont plus utilisé (en les faisant hériter d'une classe dont la seule responsabilité est de faire cette opération dans le destructeur).
Au final je vais choisir une solution plus globale. La plupart des objets de mon application sont gérés (directement ou indirectement) par une sorte de « méta-objet ». Et pour les tables, celles qui ne sont pas globales (singletons) sont des données membre de cet objet. Pour l'instant, il n'y a qu'un seul de ces objets par exécution, mais ça pourrait changer. Donc pour les tables locales, tant pis si elles se retrouvent avec des pointeurs invalides ; elles seront vidées avec la destruction du méta-objet. Pour les autres, il me suffit d'appeler une fonction qui les « nettoiera » à la destruction d'un méta-objet. Ça vous paraît choquant ? Bon je dis méta-objet, mais c'est un objet comme les autres. C'est juste que si on faisait un arbre représentant la relation « est donnée membre de », cet objet se retrouverait à la racine.
__________________
Steph |
|
|
00
|
|
|
#16 |
![]() ![]() Inscription : juin 2008 Messages : 7 494 ![]() |
Ca sent le god objet.
A la lecture de ce fil, je me demande si la première action n'est pas de reposer sur le papier ton architecture et de la revérifier. Peut être que ton appli a suffisamment évolué pour nécessiter de reprendre le design. |
|
|
00
|
|
|
#17 |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Re-bonjour.
Bon j'avais déjà commencé à revoir l'architecture depuis quelques temps. Je m'étais juste posé la question par rapport à la responsabilité de destruction, pas par rapport à la propriété. Et c'est vrai que ça change les choses. (En passant, merci Émmanuel : http://blog.emmanueldeloget.com/inde...est-%C3%A0-moi. D'ailleurs, s'il pouvait me donner son avis sur la question, ça serait cool... Alors voilà un diagramme de classes simplifié. Je ne sais pas s'il existe un moyen de l'indiquer sur le schéma, mais la multiplicité des extrémités de relation de rôle « arguments » est égale à la valeur du champ « arité » correspondant (*). Donc la classe « Programme » représente le fameux « God Object ». Les classes « Prédicat », « Symbole de fonction » et les sous-classes de « Terme » sont celles dont les instances doivent être stockées. D'un point du vue conceptuel, à part les « Entiers », toutes les informations sont sémantiquement locale à un « Programme ». Mais ces informations peuvent très bien être partagée syntaxiquement par plusieurs « Programmes ». Pour « Constante » et « Variable », je sais qui est le propriétaire, donc pas de souci (std::unique_ptr). Pour ce qui est de « Entier »… Cette classe existe essentiellement pour intégrer le type int dans la hiérarchie de base « Terme ». Alors qui est le propriétaire des instances ? Personne, je dirais. Bien sûr, on ne va créer que les instances dont on a besoin. Mais une fois que l'on n'a plus besoin d'une instance, ce n'est pas nécessaire de la garder. Je précise que l'application peut planter à cause d'un manque de mémoire (pour des problèmes très combinatoires). Je pensais faire une classe singleton pour les référencer, mais reste à savoir quoi stocker dedans… Pour « Prédicat » et « Symbole de fonction », conceptuellement ils existent indépendamment de tout « Programme », mais c'est bien un « Programme » qui leur donne un sens. Autrement dit, deux « Prédicats »/« Symboles de fonction » identiques (même nom, même arité) présent dans plusieurs programmes peuvent avoir la même sémantique, ou des sémantiques différentes. En rendre la classe « Programme » propriétaire pourrait résoudre le problème, mais ça ne me satisfait que moyennement, et on risquerait d'avoir des doublons. Et si on les stocke dans une collection statique, on arrive au même point que pour les « Entiers ». Pour les « Termes fonctionnels », c'est un peu plus compliqué. Ceux qui n'ont pas d'argument ou dont tous les arguments sont des « Entiers » sont aussi libres que les « Symboles de fonction ». Ceux qui contiennent des « Constantes » (mais pas de « Variable ») sont locaux à un « Programme ». Et ceux qui contiennent des « Variables » sont locaux à une « Règle » (un « Terme » ne peut utiliser que les variables de la « Règle » qui l'utilise). Alors qui en est le propriétaire ? Ça pourrait être « Symbole de fonction ». Mais est-ce que ça vous paraît bizarre (voire étrange…) qu'un objet soit propriétaires d'objets qui l'utilisent ? qu'un objet soit possédé par un objet qu'il utilise ? (**) Au final, le plus gros problème se porte pour les « Entiers ». Quand je saurai quelle solution adopter, je pourrai la transposer pour le reste. (*) Par exemple, un « Littéral » qui utilise un « Prédicat » d'arité 0 n'a aucun argument. Un « Terme fonctionnel » qui utilise un « Symbole de fonction » d'arité 3 a 3 arguments. (**) Oui je sais, c'est la même chose tournée différemment.
__________________
Steph |
|
|
00
|
|
|
#18 | |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Un petit UP désespéré…
Plus sérieusement, je viens de (re-)tomber sur le design pattern « flyweight » (je ne sais pas trop ce que ça donne en français), qui semble correspondre à la situation dans laquelle je me trouve. Je ne suis pas sûr d'avoir tout compris, notamment en ce qui concerne les implications. Est-ce que ça vaut le coup que je creuse d'avantage dans cette direction, ou est-ce que cela vous semble être une impasse ? Ceci dit, je remarque la présence d'un paragraphe pas vraiment anodin… Citation:
Alors une forme de comptage de références semble s'imposer (et on en revient à ce qu'on disait plus haut). Un commentaire, M. Deloget ?
__________________
Steph |
|
|
|
00
|
|
|
#19 |
|
Membre émérite
![]() Stéphane NGOMADoctorant en Informatique Inscription : septembre 2010 Messages : 669 ![]() |
Bonsoir,
Vu la tournure que prends le problème, je pense qu'un titre plus approprié pour la discussion serait le bienvenu. Malheureusement, je ne peux plus le modifier. Un administrateur/modérateur pourrait-il le faire pour moi, please ? Merci d'avance… « [Conception] Partage d'objets VS Références à un objet unique »
__________________
Steph |
|
|
00
|
|
|
#20 | |
|
Membre Expert
![]() Jean-Bernard Inscription : mars 2007 Messages : 817 ![]() |
Citation:
J'ai été récemment confronté à un problème ou j'avais du mal à définir la propriété et le cycle de vie des mes objets. Au final, ne pouvant pas tout prévoir (c'est un océan de code), j'ai implémenté plusieurs solutions et j'ai gardé celle dont le code était le plus simple. Ca demande du travail, mais en essayant plusieurs implémentations, tu tombes justement sur les problèmes que tu n'avais pas réussi à prévoir à la conception. Mais étant un essai potentiellement jetable s'il n'est pas satisfaisant : full win. |
|
|
|
00
|
Copyright © 2000-2012 - www.developpez.com