IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

unique_prt, shared_ptr ou weak_ptr ?


Sujet :

C++

  1. #1
    Membre du Club
    Inscrit en
    Mars 2007
    Messages
    134
    Détails du profil
    Informations forums :
    Inscription : Mars 2007
    Messages : 134
    Points : 55
    Points
    55
    Par défaut unique_prt, shared_ptr ou weak_ptr ?
    Bonjour,

    Pour un de mes projets, j'ai une structure avec diverses listes d'objects. Cette structure "gère" ces objets. Pour des raisons de calcul, j'ai besoin d'accéder à tous ces objects de la même manière donc les objects héritent d'une classe interface et ils ont stockés dans une "map" qui contient un pointeur vers chacun des objects.

    Mon approche marche mais n'est pas très "C++ moderne". J'aimerais remplacer les pointeurs par des "smart pointers" pour une meilleure gestion de la mémoire. Par contre, je ne les connais pas très bien et je me demande ce qu'il faut que j'utilise, j'hésite entre :
    - des shared_pointers mais la "map" est pour moi juste un moyen d'accéder aux objects et n'est pas censée les "posséder" (au sens ownership)
    - des unique_pointers pour le stockage des objects et des weak_pointers dans la "map" d'accès.

    Pourriez-vous m'aider s'il vous plaît ? Merci d'avance !

    Pour être plus concret, voisi un "pseudo code" :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    class objectInterface {
      public:
      objectInterface() {}
      virtual ~objectInterface() {}
      std::string getId() const {return m_id};
     
      private:
      std::string m_id;
    }
     
    class object1 : public objectInterface {
      public:
      object1() : objectInterface() {}
      void loadCharacteristics(/*XML file*/) {/*load characteristics*/}
    }
     
    class object2 : public objectInterface {
      public:
      object2() : objectInterface() {}
      void loadCharacteristics(/*XML file*/) {/*load characteristics*/}
    }
     
    class container {
      public:
      container() {}
      void loadObjects() {
         while(/*read an XML file*/) {
           if(/*object1*/) {
             object1 newObject;
             newObject.loadCharacteristics(/*XML file*/);
             m_myObjects1.emplace_back(newObject);
             m_objects.emplace(newObject.getId(), &newObject);
           }
           else if(/*object2*/) {
             /* Idem object 1 */
           }
         }
      }
      void clearObjects() {
        m_myObjects1.clear();
        m_myObjects2.clear();
        for(auto & [id, obj]:m_objects) {
          if(obj!=nullptr)
            delete obj;
        }
      }
     
      private:
      std::vector<object1> m_myObjects1;  // shared ou unique ptr ?
      std::vector<object2> m_myObjects2;  // shared ou unique ptr ?
      std::unordered_map<std::string, objectInterface *> m_objects;  // accès par shared_ptr ou weak_ptr ?
    }

  2. #2
    Membre chevronné
    Profil pro
    Inscrit en
    Juillet 2006
    Messages
    1 291
    Détails du profil
    Informations personnelles :
    Localisation : France, Paris (Île de France)

    Informations forums :
    Inscription : Juillet 2006
    Messages : 1 291
    Points : 1 889
    Points
    1 889
    Par défaut
    Salut,

    Pose toi les bonnes questions:
    Qui est propriétaire ?
    Une seule classe ? ==> std::unique_ptr.
    Plusieurs classes ==> std::shared_ptr.
    (je n'ai pas assez de connaissances pour répondre ce qui concerne std::weak_ptr).

    Quand aux accès, contentes-toi d'écrire des accesseurs qui renvoient une référence.

    Par défaut, je partirais sur std::unique_ptr, voir selon le comportement attendu de ton conteneur, faire un héritage privé à partir de std::map (ainsi il sera compatible avec la STL).

  3. #3
    Rédacteur/Modérateur


    Homme Profil pro
    Network game programmer
    Inscrit en
    Juin 2010
    Messages
    7 125
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 37
    Localisation : Canada

    Informations professionnelles :
    Activité : Network game programmer

    Informations forums :
    Inscription : Juin 2010
    Messages : 7 125
    Points : 33 029
    Points
    33 029
    Billets dans le blog
    4
    Par défaut
    weak_ptr c'est une soft référence vers un shared_ptr donc le mentionner avec unique_ptr est totalement faux.

    une "map" qui contient un pointeur vers chacun des objects
    la "map" est pour moi juste un moyen d'accéder aux objects et n'est pas censée les "posséder" (au sens ownership)
    et qui les possède alors si c'est pas la map ?
    Pensez à consulter la FAQ ou les cours et tutoriels de la section C++.
    Un peu de programmation réseau ?
    Aucune aide via MP ne sera dispensée. Merci d'utiliser les forums prévus à cet effet.

  4. #4
    Membre actif
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juillet 2018
    Messages
    99
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Tarn (Midi Pyrénées)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juillet 2018
    Messages : 99
    Points : 223
    Points
    223
    Par défaut
    Bonjour Julieng31,


    Il faut se dire que le shared_ptr est plus coûteux qu'un unique_ptr en terme de performance et de mémoire. En effet :
    • un unique_ptr gère le cycle de vie d'un objet de la même manière que si tu faisais toi-même le new/delete. Je pense même qu'un programme compilé en mode optimisé serait équivalent.
    • un shared_ptr gère un compteur de référence, et même si je ne sais pas exactement comment il est implémenté, il doit en toute logique créer une "indirection" du cycle de vie pour la rendre plus flexible. (J'utilise le terme "indirection" de manière volontairement floue, pour vraiment avoir les détails, je te laisse chercher sur internet)


    Donc il vaut mieux utiliser un shared_ptr seulement s'il apporte réellement un intérêt, qui serait, de manière générale, dans les cas où tu ne sais pas qui sera le dernier à manipuler tes objets. Même si ça revient au même que ce qu'à dit Deedolith, je formule ça différemment parce que je pense au cas suivant : Même si tes objets sont initialement stockés dans ton "container", est-ce que tu veux permettre aux utilisateur de conserver ces objets après la destruction du containeur ?

    Bon, maintenant supposons qu'on veuille voir le cas le plus classique, où l'utilisateur n'a pas besoin des objets quand le container est détruit. Je partirais donc sur le fait d'utiliser des unique_ptrs. Mais ta proposition "des unique_pointers pour le stockage des objects et des weak_pointers dans la map d'accès" ne marchera pas, parce qu'un weak_ptr est obligatoirement associé à un shared_ptr. Et cela pour notamment deux raisons :
    • parce que les unique_ptrs, comme je disais, sont trop "simples" pour pouvoir être attachés à des weak_ptr
    • parce que tu n'as pas le droit d'accéder à un weak_ptr directement. A la place, tu dois le "locker" pour créer un shared_ptr (cf l'exemple dans la doc)


    Maintenant, si on met des unique_ptr dans les vectors, qu'est-ce qu'on met dans la map ?
    A vrai dire, moi, pour plus de simplicité, je mettrais simplement des pointeurs
    Et la solution plus verbeuse mais surement plus "propre", c'est d'utiliser des std::reference_wrapper. Mais je n'ai pas d'expérience avec eux.
    Malgré mon peu d'expérience, je vais quand même partir sur ça, puisque mon post a quand même pour but d'être "éducatif".
    Les gens n'aiment pas les pointeurs parce que le fait que ça puisse changer de pointage et que ça puisse être nul rend la gestion de mémoire moins sûre.
    En revanche, les "références" (&) sont plus sûres parqu'elles évitent ces deux caractéristiques.
    Mais les containers comme ta map, eux, ont besoin de ces caractéristiques pour pouvoir manipuler leur contenu.
    C'est en quelque sorte pour ça qu'ont été créés les std::reference_wrapper : avec la sécurité des références mais pouvoir être utilisé dans les containers.
    Et les références fonctionnent bien dans ton cas parce que:
    • les objets stockés ne seront jamais nuls
    • l'objet associé à un id ne changera jamais
    • les cycles de vie des objets qu'ils pointent est assuré par ta classe container


    Bon, voilà, je t'offre donc le code "propre" directement pour t'aider à rentrer plus rapidemment dedans, mais si tu pars sur ça, il faudra quand même que tu apprennes les méthodes de manipulation de ces objets.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    #include <unordered_map>
    #include <vector>
    #include <memory>
    #include <functional>
     
    class objectInterface {
      public:
      objectInterface() {}
      virtual ~objectInterface() {}
      std::string getId() const {return m_id;};
     
      private:
      std::string m_id;
    };
     
    class object1 : public objectInterface {
      public:
      object1() : objectInterface() {}
      void loadCharacteristics(/*XML file*/) {/*load characteristics*/}
    };
     
    class object2 : public objectInterface {
      public:
      object2() : objectInterface() {}
      void loadCharacteristics(/*XML file*/) {/*load characteristics*/}
    };
     
    class container {
      public:
      container() {}
      void loadObjects() {
         while(/*read an XML file*/) {
           if(/*object1*/) {
             auto newObject = std::make_unique<object1>();
             newObject->loadCharacteristics(/*XML file*/);
             m_objects.emplace(newObject->getId(), std::ref(*newObject));
             m_myObjects1.emplace_back(std::move(newObject));
           }
           else if(/*object2*/) {
             /* Idem object 1 */
           }
         }
      }
      void clearObjects() {
        m_myObjects1.clear();
        m_myObjects2.clear();
        m_objects.clear();
      }
     
      private:
      std::vector<std::unique_ptr<object1>> m_myObjects1;  // shared ou unique ptr ?
      std::vector<std::unique_ptr<object2>> m_myObjects2;  // shared ou unique ptr ?
      std::unordered_map<std::string, std::reference_wrapper<objectInterface>> m_objects;  // accès par shared_ptr ou weak_ptr ?
    };
    Ah, et au fait, j'ai proposé tout ça pour suivre ton idée initiale. En fonction de ce que tu as réellement besoin, tu aurais pu te contenter de stocker plus simplement :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    std::unordered_map<std::string, std::unique_ptr<objectInterface>> m_objects;
    ou
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    std::vector<std::unique_ptr<objectInterface>> m_myObjects; // Pour itérer plus efficacement sur les valeurs
    std::unordered_map<std::string, std::reference_wrapper<objectInterface>> m_objectsMap;

  5. #5
    Membre du Club
    Inscrit en
    Mars 2007
    Messages
    134
    Détails du profil
    Informations forums :
    Inscription : Mars 2007
    Messages : 134
    Points : 55
    Points
    55
    Par défaut
    Bonjour et merci pour vos réponses !

    deedolith, AbsoluteLogic, en effet, les uniques pointers étaient ma solution "initiale" mais j'étais embêté avec le contenu de ma "map". Je n'ai mis qu'un petit exemple mais j'ai besoin d'avoir cette double structuration vecteur conteneur & map pour des besoins de calcul mais aussi de représentation "matérielle/physique" des objets qui sont du "hardware". Ce qui m'a amené aux unique pointers est en effet que le cycle de vie est porté par le vecteur. La map ne permet que d'accéder aux données plus efficacement (d'ailleurs dans la fonction clearObjects, les 2 sont "nettoyés" en même temps.

    Je n'avais pas compris que les weak pointers ne fonctionnaient que basé sur des shared pointers, en effet, mon idée ne marche pas !

    AbsoluteLogic, je ne connaissais pas les reference wrapper, je vais regarder, merci ! Est-ce que le fait d'avoir des objets représentés hérités d'une classe interface et de devoir dans mon code "dynamic cast" de la classe mère vers la classe fille ne posera pas de problème avec ces reference wrapper ?

  6. #6
    Membre du Club
    Inscrit en
    Mars 2007
    Messages
    134
    Détails du profil
    Informations forums :
    Inscription : Mars 2007
    Messages : 134
    Points : 55
    Points
    55
    Par défaut
    Je viens de tester et j'ai bien le soucis j'évoquais. La ligne suivante ne compile pas :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
     
    object1 * myObject = dynamic_cast<object1 *>(container.getObjects().at("id"));
    avec:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
     
    std::unordered_map<std::string, std::reference_wrapper<objectInterface>> & container::getObjects() {
      return m_objects;
    }
    Est-ce que je passe à côté de quelque chose ?

  7. #7
    Expert éminent
    Homme Profil pro
    Ingénieur développement matériel électronique
    Inscrit en
    Décembre 2015
    Messages
    1 572
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 61
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement matériel électronique
    Secteur : High Tech - Électronique et micro-électronique

    Informations forums :
    Inscription : Décembre 2015
    Messages : 1 572
    Points : 7 685
    Points
    7 685
    Par défaut
    Un reference_wrapper ça fonctionne comme une référence. Sa particularité c'est que contrairement aux références on peut les mettre dans des collections. Donc comme toute référence tu peux faire un cast vers une référence dans la hierarchie.
    Mais tu fais un cast de référence vers pointeur! il faut prendre l'adresse puis caster en pointeur, ou caster vers une référence.

    Et attention en prenant un vector comme responsable de tes entités. Toute modification du vector va casser toute référence ou pointeur sur ses éléments!
    Si on considère que tu ajoutes/retranche dynamiquement des objets si tu ne le fais qu'aux extrémités, tu peux utiliser un std::deque à la place du std::vector<>, ça s'utilise pareil c'est un petit peu moins performant (en vitesse et en taille).
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    private:
        std::deque<std::unique_ptr<object1>> m_myObjects1;  // 
        std::deque<std::unique_ptr<object2>> m_myObjects2;  //
        std::unordered_map<std::string, std::reference_wrapper<objectInterface>> m_objects;
    };
     
          object1 * myObject = dynamic_cast<object1*>(   &   container.getObjects().at("id"));
    Mais le dynamic_cast est rarement une bonne solution

  8. #8
    Membre actif
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juillet 2018
    Messages
    99
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Tarn (Midi Pyrénées)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juillet 2018
    Messages : 99
    Points : 223
    Points
    223
    Par défaut
    Citation Envoyé par julieng31 Voir le message
    Est-ce que je passe à côté de quelque chose ?
    Oui, tu passes à côté de pas mal de choses , plus que le dynamic_cast :
    • La programmation orientée objet consiste à ce que ton container doit assurer l'intégrité de ta structure interne. En exposant ta map interne, l'utilisateur qui utilise ton containeur pourrait modifier ta map sans modifier les vectors, ce qui serait inconsistant. Il faut au moins retourner ta map en const dans ce cas.
    • Le std::reference_wrapper n'est là que pour résoudre le problème de mettre une référence dans ta map de container. Ce serait bien de ne pas exposer son utilisation en dehors du container. Et tant qu'à faire, ne pas exposer le "unordered_map" en dehors de ton container. Peut-être qu'un jour, tu voudras remplacer ton std::unordered_map par un std::map ou par un vector de pairs, sans avoir à refaire tout le code de ce qui utilise ton container. Donc pour ça, il faut définir tes propres méthodes d'accès à ton container, notamment par l'id.
    • Maintenant, on peut aborder le dynamic_cast. Première chose à dire, éviter de l'utiliser . De mon point de vue, pour une question de performance et de compatibilité. Les fonctions virtuelles peuvent très bien s'occuper de ce boulot, cf l'exemple donné ici. Tu cherches à améliorer la robustesse de ton code avec les smart_pointeurs, mais le dynamic_cast va dans le sens contraire. Beaucoup de personnes disent que ça n'aurait jamais dû être intégré au C++ (couplé avec la notion de RTTI).
    • Mais le dynamic_cast est plus simple à utiliser. Donc si tu veux l'utiliser, sache qu'il convertit des pointeurs en pointeurs. Donc il faut convertir ton std::reference_wrapper en pointeur. Pour ça il faut récupérer la référence classique avec XXX.get(), puis prendre le pointeur correspondant avec &(XXX.get())


    Voilà, donc maintenant un peu de code.
    Pour les méthodes de container :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
      /**
       * Throws if id not found
       */
      objectInterface& getObjectRefFromId(const std::string& id) const {
        return m_objects.at(id).get();
      }
     
      /**
       * Returns nullptr if id not found
       */
      objectInterface* getObjectPtrFromId(const std::string& id) const {
        auto objectIt = m_objects.find(id);
        if (objectIt == m_objects.end())
          return nullptr;
        return &objectIt->second.get();
      }
    Pour l'accès en dehors du container (en considérant le dynamic_cast) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
        object1* myObject = dynamic_cast<object1*>(&(cont.getObjectRefFromId("id")));
        // OU
        object1* myObject = dynamic_cast<object1*>(cont.getObjectPtrFromId("id"));


    Citation Envoyé par dalfab Voir le message
    Et attention en prenant un vector comme responsable de tes entités. Toute modification du vector va casser toute référence ou pointeur sur ses éléments!
    Attention, non, ce n'est pas le cas. Ca aurait été le cas avec des "std::vector<object1>", où les objets sont stockés côte à côte. Mais il n'y a pas de problème avec std::vector<std::unique_ptr<object1>>" où chaque objet est alloué à une place fixe initiale et ne bouge pas.

  9. #9
    Membre du Club
    Inscrit en
    Mars 2007
    Messages
    134
    Détails du profil
    Informations forums :
    Inscription : Mars 2007
    Messages : 134
    Points : 55
    Points
    55
    Par défaut
    Merci encore dalfab et AbsoluteLogic pour vos retours !

    Peut être que je peux en dire un peu plus sur mon projet car vos avis me permettront peut être de mieux structurer mon code.

    En fait, je modélise des équipement électroniques. Les vecteurs d'objets 1 & 2 que je représente dans mon exemple simple est en fait une structure de donnée assez large où les équipement contiennent des cartes qui contiennent des puces, qui contiennent des ports électroniques... Cette structure représente la physique de ces équipements (que je reçois sous format XML). Cette structuration me permet simplement de définir, par exemple, quand un équipement est en panne que toutes les puces à l'intérieur sont en panne, ou de calculer la consommation électrique d'un équipement en itérant à l'intérieur de la classe associée etc...

    Uniquement certains composants participent à la transmission de l'information. Ces composants, qui sont un éparpillés dans la structure de données, héritent tous d'une classe interface qui va me permettre pas exemple de récupérer leur type (1 type par dérivation de la classe interface). Les chemins d'information sont représentés par une série de composants de ma structure de données, chacun identifié par un identifiant. Pour pouvoir "suivre"simplement un chemin d'information, j'utilise ma fameuse map. Du coup, pour propager un signal, je vais chercher les composants dans la map, je sais récupérer leur type (via la classe d'interface) et je fais un dynamic_cast (tout en sachant déjà que c'est possible et que le cast est possible).

    Un exemple de modèle de données :
    1. équipement id = "eq 1"
    1.1 carte id = "carte 1.1"
    1.1.1 puce id = "puce 1.1.1" [hérite de ma classe interface - type A]
    1.1.2 puce id = "puce 1.1.2" [hérite de ma classe interface - type A]
    1.2 carte id "carte 1.2"
    2. équipement id = "eq 2"
    2.1 carte id = "carte 2.1"
    2.1.1 puce 3 id = "puce 2.1.1" [hérite de ma classe interface - type B]

    Un exemple de chemin :
    Chemin 1 = "puce 1.1.1" suivi de "puce 2.1.1" composé d'un type A puis d'un type B

    Calcul :
    Chemin débute par "puce 1.1.1" -> vérification qu'elle est dans la map -> vérification par la classe interface que c'est le bon type -> dynamic_cast pour récupérer les infos particulières à ce composant
    Chemin continue par "puce 2.1.1" -> vérification qu'elle est dans la map -> vérification par la classe interface que c'est le bon type -> dynamic_cast pour récupérer les infos particulières à ce composant ... etc ...

    @AbsoluteLogic : mon exemple était un peu simple mais oui, j'ai bien des fonctions d'accès pour ne pas exposer la map.

    Si vous avez des suggestions, je les prends avec plaisir ! Merci d'avance ;-)

  10. #10
    Membre actif
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juillet 2018
    Messages
    99
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Tarn (Midi Pyrénées)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juillet 2018
    Messages : 99
    Points : 223
    Points
    223
    Par défaut
    A mon tour de te poser une question : est-ce que tu vois ce qu'est un diagramme de classe ? Je ne me rends pas compte de ton expérience, de si c'est un projet professionnel, d'école, et perso.
    Mais ton projet se prête très bien à la "programmation orientée objet", dans la mesure où chaque objet de ton programme représenterait des objets ou comportements concrets.
    Un diagramme de classe permettrait de très bien faire apparaître ça en le structurant. Sans parler de C++ à ce moment. Ensuite seulement on pourra discuter de comment le C++ peut représenter ce modèle de manière robuste et efficace (sûrement sans dynamic_cast ).

  11. #11
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 139
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 139
    Points : 12 239
    Points
    12 239
    Par défaut
    Le fait d'avoir à gérer plusieurs classes de composant complexifie grandement le travail.
    Et, pour l'instant, je ne vois toujours rien qui justifie l'utilisation de plusieurs classes et pas une classe avec des champs au contenu différent.

  12. #12
    Membre du Club
    Inscrit en
    Mars 2007
    Messages
    134
    Détails du profil
    Informations forums :
    Inscription : Mars 2007
    Messages : 134
    Points : 55
    Points
    55
    Par défaut
    Je suis plutôt habitué à écrire du C++ pour faire du calcul donc avec des objets simples (plutôt des liens de composition). Pour des liens plus complexes, je fais de mon mieux... !

    J'ai tenté un exemple de diagramme. La partie statique (ie elle est chargée au début et ne bouge pas) est mon data model. La partie dynamique peut évoluer et donne les chemins d'information.

    Ma difficulté est pour représenter les liens entre les signalPath qui contiennent une jeu d'identifiants parmi ceux qui sont disponibles parmi les "signalComponents".

    J'espère que mon diagramme n'est pas trop bancal...

    Nom : uml.png
Affichages : 123
Taille : 81,0 Ko

  13. #13
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 626
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 626
    Points : 30 684
    Points
    30 684
    Par défaut
    Salut,

    Pour ton problème de base (le choix entre std::unique_ptr et std::shared_ptr, tu peux partir sur le principe de base que:

    Il est préférable d'utiliser std::unique_ptr "par défaut", dans la plupart des circonstances, sauf si tu n'as vraiment pas d'autre choix.

    Le fait est que, si tu peux en effet déterminer sans l'ombre d'un doute qui est vraiment responsable de la durée de vie de la ressource associée à ton pointeur, qui a le droit "de vie et de mort" sur cette ressource et quand il va appliquer ce droit, cela va te simplifier énormément la vie, vu qu'il n'y aura plus qu'un seul moment où tu devras te poser la question de "dois-je liquider cette ressource ?".

    Dans 99 cas sur 100, il est possible -- quitte à réorganiser un peu son travail -- de déterminer ces points pour être en mesure d'utiliser std::unique_ptr

    Pour ta question connexe:
    Citation Envoyé par julieng31 Voir le message
    AbsoluteLogic, je ne connaissais pas les reference wrapper, je vais regarder, merci ! Est-ce que le fait d'avoir des objets représentés hérités d'une classe interface et de devoir dans mon code "dynamic cast" de la classe mère vers la classe fille ne posera pas de problème avec ces reference wrapper ?
    De manière générale, le transtypage, quel qu'il soit, ne devrait JAMAIS être utilisé, et c'est ** forcément ** le cas de dynamic_cast.

    C'est même encore pire en ce qui concerne le dynamic_cast (il y a quelques circonstances dans lesquelles un static_cast peut éventuellement être envisagé) que cela va t'inciter à mettre en place une logique qui brise littéralement le deuxième principe SOLID : l'OCP.

    L'OCP ( Open / Close Principle, ou, si tu préfères en français, Principe Ouvert / Fermé) stiupule en effet que tu ne devrais pas avoir à modifier un comportement qui a été validé -- sauf pour le corriger ou l'améliorer -- afin de te permettre d'ajouter une nouvelle fonctionnalité.

    Or, si tu as une hiérarchie de classes "classique", avec une classe de base et plusieurs classes dérivées, dans le style de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Base{
    public:
        virtual ~Base();
        /* le reste n'a que peu d'importance */
     
    };
    class Derivee : public Base{
        /* le reste n'a que peu d'importance */
    };
    class AutreDerivee : public Base{
        /* le reste n'a que peu d'importance */
    };
    et que tu en viens,dans une foncion quelconque à devoir écrire un code proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     void foo( Base * ptrBase){
        if(dynamic_cast<Derivee*>(ptrBase) ){
            /* on fait quelque chose */
        }else if if(dynamic_cast<AutreDerivee*>(ptrBase) ){
            /* on fait autre chose */
       }
    }
    cela va t'obliger, si tu souhaite "un jour" ajouter une classe dérivée supplémentaire, à reprendre le code de cette fonction afin d'y ajouter la possibilité de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    else if (dynamic_cast<NouvelleDerivee*> (ptrBase) ){
        /* on fait encore autre chose */
    }
    Le problème, c'est que, du coup, tu dois modifier le comportement de la fonction foo, qui est pourtant validé en l'état (tu as pu démontrer que la fonction fait effectivement ce que l'on attend de sa part) simplement pour être en mesure d'ajouter cette nouvelle classe.

    Et pire encore : comme cette technique fonctionne indéniablement, on peut s'attendre à ce que tu l'aie utilisée dans dix, quinze ou peut-être même une centaine de fonctions différentes, en fonction de la complexité de ton projet.

    Tu devras donc aller modifier ton code dans chacune des fonctions utilisant cette possibilité pour pouvoir rajouter ta nouvelle classe. Et tu vas forcément en oubliier, ce qui provoquera des bugs du genre "pourquoi quand j'appelle telle fonction (que tu as modifiée), tout fonctionne bien, et quand j'appelle telle autre (que tu as oubliée), ca ne donne plus le résultat attendu?"

    Le principe de base est donc que, si tu as été "assez bête" que pour oublier le type réel d'une donnée, tu dois baser ta logique sur le type "pour lequel ta donnée passe".

    Il y a donc une règle absolue pour ce qui concerne l'héritage publique : le LSP (Liskov Substitution Principle ou principe de substitution de Liskov, le L de SOLID) doit impérativement être respecté et va servir de "GO / NO GO" conceptuel dans le choix d'appliquer ou non un héritage.

    Afin de respecter ce principe, il y a trois points à vérifier:
    1. les préconditions ne peuvent pas être renforcées (plus strictes) dans les classes dérivées. C'est la raison pour laquelle on ne pourra pas faire hériter la classe Carre de la classe Rectangle (il y a une précondition dans Carré: longueur == largeur, qui est plus ferme que ... l'absence de toute précondition dans la classe Rectangle)
    2. les postconditions ne peuvent pas être allégée dans les classes dérivées. c'est la raison pour laquelle on ne pourra pas faire hériter la classe Rectangle de la classe Carre (il y a une postcondition dans Carre : longueur == largeur, qui n'existe simplement pas dans la classe Rectangle).
    3. L'ensemble des invariants de la classe de base est respecté dans la classe dérivée. Par invariant, il faut comprendre
      • l'ensemble des fonctions, qu'elles soient publiques ou non, valides pour la classe de base doivent impérativement être valides pour la classe dérivée
      • l'ensemble des données qui ont du sens pour la classe de base doivent avoir du sens pour la classe dérivée
      • l'ensemble des conditions permettant à la classe de base de "fonctionner correcttement" doivent être valides pour la classe dérivée

    En d'autre termes, si tu ne peux déjà pas utiliser ta classe dérivée exactement comme s'il s'agissait d'une instance de la classe de base, alors, c'est que tu ne peux pas tracer la relation d'héritage entre les deux classes. Et ca, c'est un "GO / NO GO" conceptuel.

    C'est à dire que, peu importe ce que l'on a pu te dire à l'école (qu'un carré est un rectangle, par exemple), peu importe que le langage autorise ou non la relation d'héritage (dans le cas d'héritage multiple, par exemple), peu importe ce que te dira le compilateur (qui ne fait que respecter les règles de base imposées par le langage), si les règles que je viens de citer ne sont pas respectées AVANT MÊME D'ECRIRE LA PREMIERE LIGNE DU CODE qui fera hériter ta classe dérivée de ta classe de base, c'est que tu ne peux pas prendre la décision de faire hériter ta classe dérivée de ta classe de base. Et tu dois donc trouver une autre solution.

    Cependant, il existe effectivement des circonstances dans lesquelles on pourrait souhaiter être en mesure de manipuler une instance de la classe dérivée en tant ... que cette classe dérivée elle-même, malgré le fait qu'on la connaisse comme étant une instance "de la classe de base".

    Dans ce genre de cas, la solution à envisager va passer par ce que l'on appelle le double dispatch. L'idée de base de ce concept est que, même si nous ne connaissons une donnée que comme "étant une instance de la classe de base", la donnée en elle-même sait encore très bien quel est son type réel. On va donc utiliser un comportement de la classe de base qui permette à la classe dérivée de se transmettre à ... deuxième comportement, en tant qu'instance de la classe dérivée.

    Et comme ce deuxième comportement saura pertinemment quel les le type (réel, cette fois ci) de la donnée, il pourra la manipuler en tant que telle.

    Ce concept peut prendre la forme d'un patron de conception (design pattern) appelé "visiteur", qui pourrait ressembler à ceci:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    /* D'un coté, nous avons une classe "Visiteur", qui peut être dérivé en différent types de visiteur "concret" et 
     * qui expose des comportements spécifiques pour chacune des classes dérivée
     * 
     * Pour satisfaire le compilateur, il faut dresser la liste des classes susceptibles d'être manipulées ;-)
     */
    class Base; // la classe de base
    class Derivee; // une premiere classe dérivée
    class AutreDerivee; // une deuxième classe dérivée
    class NouvelleDerivee; // la classe dérivée que l'on ajoute plus tard (on ajoute cette ligne lors de l'ajout de la classe :D )
    /* ... on peut en avoir tout plein, bien sur */
    /* Cela nous permet de déclarer des fonctions virtuelles prenant une référence sur ces différents types, sous la forme de */
    class Visiteur {
    public:
        /* dans un premier temps, on ne sait pas ce qu'il fera de ces données, on va donc les déclarer comme fonctions virutelles pures */
        virtual void visit( Derivee /*const*/ & d) = 0; 
        virtual void visit(AutreDerivee /* const*/ & d) =0;
        virtual void visit(NouvelleDerivee /* const*/ & d) =0;
        /* on fera bien sur cela pour toutes les classes dérivées */
    };
     
    /* Et, d'un autre coté, nous aurons donc la hiérarchie de classes  qui nous intéresse qui exposera un comportement
     * permettant ... d'accepter le visiteur.  C'est ce comportement qui sera défini pour chacune des classes dérivées, sous la forme
     * de
     */
    class Base{
    public:
        /* Les hierachies de classes ont, typiquement, sémantique d'entité.  On ne veut en aucun cas pouvoir utiliser le constructeur de copie
         * ou l'opérateur d'affectation
         */
        Base(Base const &) = delete;
        Base & operator= (Base const &) = delete;
        /* C++11 est arrivé avec les notions d'affectation et de copie par déplacement,  on ne veut pas d'avantage pouvoir y avoir recours */
        Base(Base && ) = delete;
        Base & operator=(Base && ) = delete;
        /* on voudra par contre sans doute pouvoir détruire n'importe quelle instance d'une classe dérivée alors que nous 
         * la connaissons comme étant une instance de la classe de base
         */
        virtual ~Base(); = default;
        /* La magie du visiteur : le comportement accept, que nous devrons définir pour chacune des classes dérivées.
         * Comme nous ne pouvons pas fournir de comportement adéquat pour la classe de base, nous en faisons une fonction
         * virtuelle pure
         */
        virtual void accept(Visiteur /* const */ & ) = 0;
    };
    /* dans les classes dérivées, nous implémentons le comportement accept, toujours de la même manière : en appelant 
     * le comportement visit du visiteur, et en lui transmettant la donée elle_même
     */
    class Derivee : public Base{
    public:
        void accept(Visitor /* const*/  & v) override{
            v.visit(*this);
        }
        /* Il peut y avoir d'autres trucs ici */
    };
    class AutreDerivee : public Base{
    public:
        void accept(Visitor /* const*/  & v) override{
            v.visit(*this);
        }
        /* Il peut y avoir d'autres trucs ici */
    };
    class NouvelleDerivee : public Base{
    public:
        void accept(Visitor /* const*/  & v) override{
            v.visit(*this);
        }
        /* Il peut y avoir d'autres trucs ici */
    };
    /* et bien sur, nous le ferons pour toutes les classes dérivées. */
     
    /* Enfin, chaque fois que nous voudrons faire "quelque chose de spécial" avec nos données 
     * (qui sont, en réalité, des instances des classes dérivées, mais que nous connaissons comme étant
     * des instances de la classe de base), nous pourrons créer un visiteur (concret, celui-i) pour lequel
     * nous définissons le comportement visit, adapté à chacun des types des classes dérivées
     */
     class VisiteurConcret : public Visiteur {
    public:
        /* dans un premier temps, on ne sait pas ce qu'il fera de ces données, on va donc les déclarer comme fonctions virutelles pures */
        void visit( Derivee /*const*/ & d) override{
            /* je sais manipuler une instance de la classe Derivee, que dois-je faire ici? */
        } 
        void visit(AutreDerivee /* const*/ & d) override{
            /* je sais manipuler une instance de la classe AutreDerivee, que dois-je faire ici? */
        } 
        void visit(NouvelleDerivee /* const*/ & d) override{
            /* je sais manipuler une instance de la classe NouvelleDerivee, que dois-je faire ici? */
        } 
        /* on fera bien sur cela pour toutes les classes dérivées */
    };
     
    /* Tout cela nous permettra de travailler "facilement" dans la plupart des situations, par exemple, une fonction comme celle ci
     */
    int main(){
        std::vector<std::unique_ptr<Base>> datas; // C'est dans ce tableau que je maintiens l'ensemble des données
        /* je remplis mon tableau d'une manière ou d'une autre */
        /* puis vient le moment où je veux les utiliser
         * je crée donc l'instance de mon visiteur concret */
        VisiteurConcret visiteur;
        /* et, pour chacune des données que je trouve dans le tableau, */
        for(auto & ptr : datas){
            /* je demande à la donnée d'accepter mon visiteur */
           ptr->accept(visiteur);
        }
    }
    L'énorme avantage de la technique, c'est que, si je décide "un jour" de rajouter une classe dérivée, je devrai -- bien sur -- ajouter le comportement visit(la_nouvelle_classe) dans ma classe Visiteur et implémenter ce comportement dans l'ensemble des visiteurs concrêts que j'utilise, MAIS je ne dois absolument pas toucher aux différents comportements que j'ai déjà mis en place pour les autres classes (hormis, bien sur, le fait de prévoir la création des instances de cette nouvelle classe). Et donc, je réduis considérablement le risque de "tout casser", vu que je n'ai plus rien à modifier...

    Alors, bien sur ... Cette technique présente quelques inconvénients. Dont celui de devoir implémenter le comportement de cette nouvelle fonction visit dans l'ensemble des visiteurs concrêts, ce qui aura sans doute un impact sur l'ABI. Pour faire simple, si tu développe une DLL, l'ajout d'une nouvelle classe t'obligera à utiliser la nouvelle verison de cette DLL sous peine d'avoir peut être quelques soucis.

    Cependant, cela peut apporter une telle sérénité dans ton développement, et, surtout, te faire gagner tellement de temps

    Ah, et un petit truc en plus : à partir du moment où tu envisages d'utiliser l'héritage, tu devrais envisager tes classes sur base de leur comportements (des fonctions, virtuelles ou non, auquellles tu estimes devoir pouvoir accéder), plutôt que sur base de leurs données.

    Car ce sont les comportements qui permettent le mieux de déduire le type de l'objet auquel on a afffaire ( "Si mon objet marche comme un canard, qu'il cancanne comme un canard et qu'il nage comme un canard, il est très possible que j'ai affaire à un canard ). Les données qui composent l'objet n'étant en définitive là que pour ... permettre aux différents comportements de fournir le résultat attendu.
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  14. #14
    Membre actif
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juillet 2018
    Messages
    99
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Tarn (Midi Pyrénées)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juillet 2018
    Messages : 99
    Points : 223
    Points
    223
    Par défaut
    Citation Envoyé par julieng31 Voir le message
    J'ai tenté un exemple de diagramme.
    Bonjour Julien, le diagramme de classe nous aiderait à donner des réponses un peu plus éclairées à tes questions, mais je n'ai quand même pas la motivations de choisir/rédiger la structure C++ pour tous les composants. Donc en tout cas je suis dispo pour te répondre si tu as des doutes sur des points précis. Et typiquement si tu ne vois pas comment ne pas utiliser le dynamic_cast dans un certain cas concret (si possible avec le contexte autour, donc notamment le diagramme), tu peux nous faire part de ce cas qui servira d'exemple.

    Une petite remarque concernant ton diagramme quand même, il y a beaucoup de "identifier". L'idée est de le mettre dans une classe mère, sans le remettre dans les classes filles s'il s'agit du même type d'identifier. Et s'il y a différents types d'identifiers, il faut les nommer différemment.

    Voilà, on s'éloigne un peu du titre de la discussion, mais j'imagine tant pis ^^

  15. #15
    Membre du Club
    Inscrit en
    Mars 2007
    Messages
    134
    Détails du profil
    Informations forums :
    Inscription : Mars 2007
    Messages : 134
    Points : 55
    Points
    55
    Par défaut
    C'est vrai que cela dépasse un peu la question initiale mais cela m'a permis d'apprendre qu'il faut éviter le dynamic_cast et de réfléchir à nouveau sur la structuration des mes données ;-) Merci pour vos conseils précis et précieux !

    Je clos le post mais continue d'améliorer mon approche !

  16. #16
    Expert éminent sénior
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Février 2005
    Messages
    5 139
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : France, Val de Marne (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Conseil

    Informations forums :
    Inscription : Février 2005
    Messages : 5 139
    Points : 12 239
    Points
    12 239
    Par défaut
    Pourquoi "Connexion" et "Plug" devraient être des classes sœurs ???
    Pourquoi ne pas les mettre dans 2 listes distinctes ? (la seul contrainte que je vois, c'est qu'elles ne doivent pas avoir d'identifiant commun)

  17. #17
    Membre du Club
    Inscrit en
    Mars 2007
    Messages
    134
    Détails du profil
    Informations forums :
    Inscription : Mars 2007
    Messages : 134
    Points : 55
    Points
    55
    Par défaut
    En effet, l'idée était de gérer d'une part l'unicité d'identifiants (ce qui expliquait le choix d'une map) mais aussi d'avoir "sous la main" tous les composants qui participent à chemin d'information. J'y réfléchissais, c'est peut être cela qui m'amène à devoir ensuite revenir aux classes filles car je ne manipule que la classe interface.

    Je pourrais avoir des listes "typées" (par composant) et avoir un accesseur qui me fournirait un élément d'un type donné s'il existe, comme ça je ne "perds" pas le type de la classe fille. Ça me fait juste une fonction d'accès par type mais ils ne sont pas trop nombreux.

  18. #18
    Membre expérimenté
    Femme Profil pro
    ..
    Inscrit en
    Décembre 2019
    Messages
    614
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Âge : 94
    Localisation : Autre

    Informations professionnelles :
    Activité : ..

    Informations forums :
    Inscription : Décembre 2019
    Messages : 614
    Points : 1 369
    Points
    1 369
    Par défaut
    Salut tout le monde,

    Citation Envoyé par julieng31 Voir le message
    ...
    Ton message m'évoque le protocole SNMP (avec ses OID, MIB).
    Si tu ne connais pas, renseigne-toi dessus, ça peut être une bonne source d'inspiration. En plus, il y a de bonnes bibliothèques et outils déjà existants.
    Maintenant, utiliser std::map, un graphe binaire pour modéliser ton réseau me semble un peu limite. Personnellement, je vois DOM comme un meilleur candidat, surtout qu'à terme il y aura probablement un besoin d'interopérabilité. Donc ça, plus la forte ressemblance avec SNMP, il y a de quoi faire quelque chose de bien.

    Bon courage pour ton projet, il est intéressant.

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Arbre shared_ptr et weak_ptr
    Par darkman19320 dans le forum C++
    Réponses: 7
    Dernier message: 30/08/2017, 22h07
  2. Passage de C++ à Java : Shared_ptr et Weak_ptr
    Par malaboss dans le forum Général Java
    Réponses: 5
    Dernier message: 25/06/2012, 18h40
  3. Réponses: 1
    Dernier message: 02/10/2011, 12h56
  4. [BOOST] shared_ptr et pointer C
    Par zdra dans le forum Bibliothèques
    Réponses: 7
    Dernier message: 08/05/2005, 14h15

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo