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

  1. #1
    Candidat au Club
    Implémentation d'un singleton avec des std::shared_ptr
    Bonjour à tous,

    Je viens à vous car je me heurte à un problème que je n'arrive pas à comprendre.
    Je tente d'implémenter une classe singleton Template réutilisable ; en m'inspirant de ce que je trouve sur le net, j'arrive à une première implémentation (non encore thread-safe, mais c'est pas la question), avec des pointeurs classiques, qui marche :

    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
     
    #ifndef DEFINE_SINGLETON
    #define DEFINE_SINGLETON
     
    #include <iostream>
     
     
    // Classe de design pattern Singleton
     
    template <typename T>
     
    class Singleton {
     
    protected :
     
      // Pointeur vers l'instance unique
      static T* singletonPtr;
     
      // Constructeur et destructeur privés pour éviter l'appel depuis l'extérieur.
      Singleton () {}
      ~Singleton() {}
     
    public :
     
      // Méthode statique pour accéder à l'instance unique du Singleton ; celle-ci est
      // construite si elle n'était pas encore allouée.
      static T* getInstance() {
        if ( singletonPtr == nullptr ) singletonPtr = new T;
        return singletonPtr;
      }
     
      // Méthode statique pour détruire l'instance unique de T
      static void kill() {
        if ( singletonPtr != nullptr) delete singletonPtr;
        singletonPtr = nullptr;
      }    
     
    };
     
    // définition du singleton statique, intialisé sur nullptr
    template <typename T>
    T* Singleton<T>::singletonPtr = nullptr;
     
    // Classe test utilisant la classe ci-dessus
     
    class Test : public Singleton<Test> {
     
      friend class Singleton<Test>;
     
    private :
     
      // Constructeur et destructeur private pour éviter l'instanciation/destruction
      Test() {}
      ~Test(){}
     
      // Constructeur de copie et opérateur d'affectation interdits
      Test(const Test& other) = delete;
      Test operator=(const Test& other) = delete;
     
    public :
     
      // une méthode quelconque
      void doSomething() { std::cout << "doSomething" << std::endl; }
     
    };
     
     
    #endif // ndef DEFINE_SINGLETON


    et le main pour tester :

    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
     
    #include <iostream>
    #include "SingletonSP.hpp"
     
    int main() {
      Test::getInstance()->doSomething();
      Test::kill();
      return 0;
    };


    Ceci compile et marche parfaitement.

    Ensuite, j'essaie une version avec des shared_ptr :

    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
     
    #ifndef DEFINE_SINGLETON
    #define DEFINE_SINGLETON
     
    #include <iostream>
    #include <memory>
     
     
    // Classe de design pattern Singleton
     
    template <typename T>
    class SingletonSP {
     
    protected :
      // Pointeur vers l'instance unique
      static std::shared_ptr<T> singletonPtr;
     
      // Constructeur et destructeur privés pour éviter l'appel depuis l'extérieur.
      SingletonSP () {}
      ~SingletonSP() {}
     
    public :
     
      // Méthode statique pour accéder à l'instance unique du Singleton ; celle-ci est
      // construite si elle n'était pas encore allouée.
      static std::shared_ptr<T> getInstance() {
        if ( !singletonPtr )  singletonPtr = std::make_shared<T>();
        return singletonPtr;
      }
     
      // kill() n'est a priori plus nécessaire, puisque singletonPtr sera détruit à la fin du scope
      // global, désalouant automatiquement l'objet pointé.
     
    };
     
    // la définition du singleton statique, non initailisée car encapsulée.
    template <typename T>
    std::shared_ptr<T> Singleton<T>::singletonPtr;
     
    // Classe test utilisant la classe ci-dessus
     
    class Test : public SingletonSP<Test> {
     
      friend class Singleton<Test>;
      friend std::shared_ptr<Test> std::make_shared<Test>(); // friend pour avoir accès au constructeur
      friend class std::shared_ptr<Test>; // friend pour avoir accès au destructeur
     
    private :
     
      // Constructeur et destructeur private pour éviter l'instanciation/destruction
      Test() {}
      ~Test(){}
     
      // Constructeur de copie et opérateur d'affectation interdits
      Test(const Test& other) = delete;
      Test operator=(const Test& other) = delete;
     
    public :
     
      // une méthode quelconque
      void doSomething() { std::cout << "doSomething" << std::endl; }
     
    };
     
     
    #endif // ndef DEFINE_SINGLETON


    et le main à peine modifié :

    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    #include <iostream>
    #include "SingletonSP.hpp"
     
    int main() {
      Test::getInstance()->doSomething(); // <-ligne 9
      return 0;
    };


    Et là, la belle erreur de compilation, qui en clair me dit qu'à la ligne 9 je fais appel aux constructeur et destructeur de Test, ce qui est interdit car ils sont private.
    L'erreur exacte du compilo est, sans toutes les inclusions de la stl (que j'ai remplacées par (...) pour rendre le truc lisible) :
    (...)
    SingletonSP.hpp:28:62: required from ‘static std::shared_ptr<_Tp1> SingletonSP<T>::getInstance() [with T = Test]’
    main.cpp:6:9: required from here
    SingletonSP.hpp:52:3: error: ‘Test::Test()’ is private
    Test() {}
    (...)
    main.cpp:9:2: required from here
    SingletonSP.hpp:53:3: error: ‘Test::~Test()’ is private
    ~Test(){}
    (...)

    Voilà, j'imagine qu'il s'agit d'une subtilité dans l'utilisation des shared_ptr que je ne vois pas, mais justement je ne la vois pas...
    Précision, j'utilise g++ 4.8.1 avec les options -Wall, -Wextra et -std=c++11

    Merci d'avance,

    whityranger

  2. #2
    Membre expert
    Est-ce vraiment une bonne idée
    Bonsoir

    je ne pense pas que le concept de singleton et de shared_ptr puisse faire bon ménage.

    Comme pour la plupart des conteneurs de la stl (probablement presque tous) l'un des pré-requis pour leur utilisation est de leur fournir des objets qui puissent-être construit par copie, Or c'est justement ce que l'on souhaite éviter avec le singleton.

    si on lit attentivement la doc d'origine
    http://www.boost.org/doc/libs/1_55_0...lt_constructor

    Every shared_ptr meets the CopyConstructible, MoveConstructible, CopyAssignable and MoveAssignable requirements of the C++ Standard Library, and can be used in standard library containers. Comparison operators are supplied so that shared_ptr works with the standard library's associative containers.
    d'ailleur le compilateur te dit clairement que pour ton exemple de code shared_ptr ne peut pas accéder ni au constructeur Test::Test(), ni au destructeur Test::~Test() car ces dernier sont privés....
    bazar: http://www.improetcompagnie.com/publ...ctacles-6.html

    BÉPO la disposition de clavier francophone, ergonomique et libre: http://bepo.fr/wiki/Accueil

    Emacs Wiki: http://www.emacswiki.org/

    En attente de ce que produira: http://www.pushmid.com

  3. #3
    Expert éminent sénior
    Salut,

    Tout comme jabbounet, j'estime que le shared_ptr n'est sans doute pas adapté, mais pour une autre raison : shared_ptr est un pointeur partagé.

    Autrement dit, shared_ptr a pour but de s'assurer que la mémoire allouée à un objet sera correctement détruite lorsque le dernier objet qui l'utilise est détruit. Ce n'est pas le but du singleton qui est de fournir un objet dont on est sur qu'il existera toujours! Ce n'est donc pas ce qu'il te faut.

    Le but du singleton est de s'assurer qu'il n'y aura à tout instant qu'une seule et unique instance de ton objet. Or, si tu veux un pointeur unique, ce serait plutôt std::unique_ptr qu'il te faudrait.

    Cela prendrait une forme proche de
    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
    template <typename T>
    class Singleton{
        public:
            /* pour respecter ton code original(*) */
            static T* instance(){
               /* rendu obligatoire à cause de la présence de kill */
               if(! instance_.get())
                   instance_.reset(new T);
                return instance_.get();
            }
            /* pour respecter ton code original (*) */
            static void kill(){
                instance_.reset();
            }
    };
    static std::unique_ptr<T> Singleton::instance_= std::unique_ptr<T>(new T);

    (*) Typiquement, un singleton est construit au début de l'exécution du programme et détruit lorsque le programme s'achève. Et, lorsque le programme s'achève, toute la mémoire allouée à ce programme est automatiquement libérée par le système.

    Mais un singleton a aussi pour but de s'assurer que nous accéderons toujours au même objet, quel que soit le nombre de fois que nous tenterons d'y accéder.

    Dés lors, on peut décemment se poser la question : Pourquoi donner à l'utilisateur l'occasion de détruire l'objet"

    Ceci dit, le problème vient de ce à quoi ton singleton accorde son amitié.

    Je m'explique : Dans ta version originale, tu déclares la classe Singleton<T> amie du singleton "concret". C'est ce qu'il faut faire, parce que c'est Singleton<T> qui va essayer d'appeler le constructeur de ton singleton concret.

    Le problème, c'est que dans ta version avec un shared_ptr, ce n'est pas Singleton<T> qui va faire appel au constructeur : c'est std::make_shared. Or, std::make_shared n'est pas une fonction membre de Singleton<T> : C'est une fonction libre.

    Tu en arrive donc à une situation dans laquelle ton singleton accorde son amitié à "quelque chose" (comprends : Singleton<T>) qui n'en a que faire, mais qui n'accorde pas son amitié à ce qui en a réellement besoin, à savoir std::make_shared.

    Tu as donc deux solution : Soit tu crées toi-même le shared_ptr, soit tu veilles à ce que ton singleton concret accorde son amitié à ce qui en a vraiment besoin

    En outre, il est tout à fait possible d'éviter le recours au pointeur et à l'allocation dynamique de la mémoire lorsque tu envisages (l'anti) le patern singleon. Tu peux parfaitement définir une variable statique directement dans la fonction instance(), en veillant alors à renvoyer une référence au lieu d'un pointeur.

    Cela prendrait la forme de
    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
    /** le concept d'objet non copiable
      *
      * j'aime disposer d'une classe qui me permette de représenter
      * le concept d'objet non copiable.  Cela permet de rendre la
      * notion plus "conceptuelle" en plus de faciliter l'écriture <img src="images/smilies/icon_wink.gif" border="0" alt="" title=";)" class="inlineimg" />
      *
      * @note Cette classe est destinée à intervenir dans une relation d'héritage privé
      */  
    struct NonCopyable{
        NonCopyable(NonCopyable const &) = delete;
        NonCopyable & operator = (NonCopyable const &) = delete;
    };
    /* La classe de base pour tous les singleton
     *
     * Le but du singleton est d'assurer l'unicité de l'objet.
     * Il ne doit donc être ni copiable ni affectable
     */
    template <typename T>
    SingletonByRef : private NonCopyable{
        public:
            T & instance(){
                 static T obj;
                 return obj;
             }
    };
    /** Mon singleton concret
       *
       * Comme SingletonByRef<T> n'est pas copiable ni affectable,
       * cette classe ne l'est pas non plus du fait de l'héritage
    class MySingleton : public SingletonByRef<T>{
        friend class SingletonByRef<T>;
        public:
            void doSomething(){}
        private:
            MySingleton(){}
            ~MySingleton(){}
    };
    qui pourrait être utilisée sous une forme proche de
    Code :Sélectionner tout -Visualiser dans une fenêtre à part
    1
    2
    3
    4
    int main(){
        MySingleton obj;
        obj.doSomething();
    }
    Cette manière de faire n'a que des avantages :
    1. Tu gardes la syntaxe d'accès "classique" au lieu d'utiliser la syntaxe dédiée aux pointeurs (bon, c'est pas le plus gros avantage, mais c'en est quand même un )
    2. Tu évites le recours au pointeurs et tous les problèmes connexes
    3. Tu es sur que l'instance de ton singleton concret sera créée en temps opportun et qu'elle sera d'office détruite à la fin de l'exécution du programme
    4. Tu n'a pas besoin d'exposer une fonction kill qui permet à l'utilisateur de détruire ton singleton alors qu'il n'a simplement pas à le faire
    Enfin, je ne peux que m'insurger une fois de plus contre l'usage de cet anti pattern!!!

    Il faut bien te dire qu'une variable statique n'est jamais qu'une variable globale habilement déguisée qui flotte sur un océan de mystère et que les variables globales, C'EST MAL.

    En plus, un singleton est, par définition, une variable qui est disponible PARTOUT. Si tu as besoin d'une telle variable, c'est très certainement que tu as un gros problème de partage des tâches dans le sens où tu permet à trop de choses différentes de faire la même chose. Avec, comme corollaire le fait que, quand tu seras confronté à un bug, tu devras parcourir tout ton code pour trouver tous les endroits où une action particulière est entreprise afin de les corriger... Avec la quasi certitude d'en oublier systématiquement.

    Le meilleur moyen de n'avoir qu'un instance d'un objet est toujours de veiller n'en créer qu'une
    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

  4. #4
    Candidat au Club
    Merci pour vos deux réponses : celle de jabbounet pour l'aspect compréhension technique, celle de koala01 pour la solution alternative plus robuste.

    Je rebondis dessus : je suis bien entendu conscient du fait que le singleton est une variable globale, et que ces dernières sont souvent source de complications au débuggage. Cependant, j'ai tendance à m'en servir uniquement pour deux types de choses : un conteneur gérant des options connues uniquement à l’exécution (en général une struct initialisée en début de programme, c'est quelque chose que j'utilise surtout dans mes codes de calcul scientifique, pour gérer l'abondance des informations affichées dans le log par exemple), et les gestionnaires de ressources. En l'occurrence, il s'agit d'un jeu et de gestionnaires de ressources (pour l'instant textures, mais à venir sons etc...).

    Donc la question que je me pose est : y a-t-il mieux ? Je n'en ai pas l'impression. Les classes qui utilisent ces ressources n'ont pas à savoir comment es autres classes les utilisent, donc il me semble logique d'avoir un gestionnaire externe. Avoir plusieurs gestionnaires serait un gâchis de mémoire. Et en déclarer un unique local au début de main pour le passer en paramètre supplémentaire à TOUTES les classes apparaissant ensuite et susceptibles de l'utiliser ou de contenir des classes susceptibles de l'utiliser me semble affreusement lourd pour pas grand-chose, d'autant plus que l'opérateur de résolution de portée permet de savoir quand on joue avec une variable globale singleton bien plus simplement qu'avec une variable globale simple.

    Bref, si vous voyez plus simple, je suis preneur...

  5. #5
    Expert éminent sénior
    Et moi je vais te poser une autre question : Quelles sont les classes qui ont vraiment besoin d'avoir accès à toutes les informations que contiennent tes singletons

    Et si tu me répond "toutes", tu passes par la fenêtre Je te suggère d'aller faire un petit tour vers une discussion qui devrait t'aider à comprendre mon idée. Après tout, elle est récente et de nombreuses voies sont explorées
    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

  6. #6
    Candidat au Club
    Alors, je n'ai pas encore décortiqué la discussion que tu as mise en lien, mais en la lisant en diagonale, j'ai l'impression que ne ressortent que quelques grands principes applicables dans mon cas, tels que "une classe doit avoir un seul et unique but bien défini", que je m'efforce de respecter (même si je ne le fais sans doute pas parfaitement... ). Le fait est qu'il ne s'agit pas ici de gérer une config dont les éléments accessibles doivent être définis avec soin, ni un transfert d'informations d'une source quelconque vers la classe qui se chargera de les traiter ; il s'agit de s'assurer qu'on ne charge en mémoire une et une seule fois des ressources partagées, et en l'occurrence un seul type de ressource (histoire de bien séparer les responsabilités) : les textures.

    Donc là-dessus, se pose ta question :

    Quelles sont les classes qui ont vraiment besoin d'avoir accès à toutes les informations que contiennent tes singletons ?
    Et la réponse n'est pas "toutes", mais dans ce cas pas loin. J'accorde volontiers que le gestionnaire des polices ne sera utile qu'aux classes de Menus (et pourrait donc être instancié comme un membre statique privé de la classe mère Menu, par exemple), que le gestionnaire des sons devra être limité à quelques classes sur lesquelles je ne me suis pas encore penché... Et dans ce cas, ok.
    Mais pour le manager de Textures, ben celles-ci sont utilisées par les menus (pour les images de fond), par les personnages, les objets à l'écran, la map, l'interface de jeu... Toutes classes qui sont assez nombreuses et n'ont pas grand-chose en commun (sauf les personnages et objets, qui dérivent d'une même classe mère Element dans ma conception actuelle).
    Je vais même plus loin : les Element n'ont pas vraiment besoin de ces Textures, ce sont des objets internes gérant l'affichage (séparation des tâches) appelés SpriteManager qui s'en servent, et ils ne s'en servent qu'à leur construction (pas besoin de garder une variable membre associée donc). Donc les Element n'ont même pas besoin eux-même de TextureManager, et il faudrait le passer à leur constructeur juste pour le transmettre à qui de droit ? Ça ne me semble pas logique comme "séparation des tâches".

    Donc :
    1. soit j'instancie un Manager unique et je transmet une référence sur mon Manager dans tous les constructeurs de toutes les classes susceptibles de les utiliser ; ça implique de le passer en argument du constructeur des Element uniquement pour le passer ensuite au constructeur des SpriteManager... et ça me semble ne pas être le job de Element de gérer les ressources. C'est peut-être plus propre, mais personnellement je trouve ça lourd si c'est juste utilisé à la construction.
    2. soit je place TextureManager::getInstance() en private et je déclare en amies SpriteManager (comme ça les autres n'y ont pas accès), mais aussi tous ceux qui ont besoin de ça parce qu'elles n'utilisent pas les SpriteManager en interne (donc la Map, et toutes les classes filles de Menu, puisque l'amitié n'est pas héritée -> au secours )
    3. soit je retravaille les Menus pour qu'ils utilisent les SpriteManager en interne pour faire la 2. et n'avoir en friend que SpriteManager et Map -> déjà mieux, mais ça me semble quand même rustine ; à savoir que pour des raisons d'optimisation, je préfère gérer Map sans SpriteManager mais de façon personnalisée
    4. soit je garde un singleton global et j'essaie de faire confiance à mon équipe de dev...

    J'ai l'impression que tu trouveras la 1. plus propre, mais je pense que cela alourdit l'écriture du code pour pas grand-chose... à moins que tu ne me prouves le contraire :p

    EDIT : je viens de penser à une 5e solution : puisque, à l'exception de Map, tous les objets affichés peuvent le faire par l'intermédiaire d'un SpriteManager, je pourrais placer TextureManager (non singleton) comme variable statique privée dans la classe SpriteManager, et déclarer les quelques méthodes (à vue de nez seulement deux) de Map qui ont besoin de TextureManager comme friend de la classe SpriteManager (pour accéder au TextureManager statique)... ainsi seuls les objets en ayant l'utilité ont accès à TextureManager, et en ont même connaissance... ça me semble plutôt pas mal, j'aimerais vos avis sur ces 5 idées

  7. #7
    Expert éminent sénior
    Citation Envoyé par whityranger Voir le message
    Alors, je n'ai pas encore décortiqué la discussion que tu as mise en lien, mais en la lisant en diagonale, j'ai l'impression que ne ressortent que quelques grands principes applicables dans mon cas, tels que "une classe doit avoir un seul et unique but bien défini", que je m'efforce de respecter (même si je ne le fais sans doute pas parfaitement... ). Le fait est qu'il ne s'agit pas ici de gérer une config dont les éléments accessibles doivent être définis avec soin, ni un transfert d'informations d'une source quelconque vers la classe qui se chargera de les traiter ; il s'agit de s'assurer qu'on ne charge en mémoire une et une seule fois des ressources partagées, et en l'occurrence un seul type de ressource (histoire de bien séparer les responsabilités) : les textures.

    Donc là-dessus, se pose ta question :


    Et la réponse n'est pas "toutes",
    Bon, ben, tu vas passer par la fenêtre
    mais dans ce cas pas loin. J'accorde volontiers que le gestionnaire des polices ne sera utile qu'aux classes de Menus (et pourrait donc être instancié comme un membre statique privé de la classe mère Menu, par exemple), que le gestionnaire des sons devra être limité à quelques classes sur lesquelles je ne me suis pas encore penché... Et dans ce cas, ok.
    Mais pour le manager de Textures, ben celles-ci sont utilisées par les menus (pour les images de fond), par les personnages, les objets à l'écran, la map, l'interface de jeu... Toutes classes qui sont assez nombreuses et n'ont pas grand-chose en commun (sauf les personnages et objets, qui dérivent d'une même classe mère Element dans ma conception actuelle).
    Je vais même plus loin : les Element n'ont pas vraiment besoin de ces Textures, ce sont des objets internes gérant l'affichage (séparation des tâches) appelés SpriteManager qui s'en servent, et ils ne s'en servent qu'à leur construction (pas besoin de garder une variable membre associée donc). Donc les Element n'ont même pas besoin eux-même de TextureManager, et il faudrait le passer à leur constructeur juste pour le transmettre à qui de droit ? Ça ne me semble pas logique comme "séparation des tâches".
    Héhé... Raté!!!

    Ni ta map, ni tes perso, ni tes menus ni tes sprites n'ont besoin ni des textures ni des polices de caractères pour fonctionner! Ca t'en bouche un coin ca, hein

    Mais ne t'en fais pas, tu commets là une erreur classique : tu confonds ce que tu vois à l'écran avec tes données métier.

    Sauf que, tes données métier, elles se foutent pas mal de savoir à quoi elles ressemblent! Si elles ressemblent à quelque chose, c'est à une succession de 1 et de 0

    Et la deuxième erreur que tu commets, c'est de vouloir que ce soient tes données métiers qui s'occupe de s'afficher dans ta vue.

    Mais si tu prends le problème en sens inverse, que tu te dis que c'est la vue qui s'occupe d'afficher les données métiers qu'on lui soumet, il n'y a plus qu'un seul endroit où tout ce qui permet l'affichage doit être regroupé. Et cet endroit, il sera de toutes façons unique, et tu sais pourquoi parce que ce sera l'endroit où ta fenêtre d'affichage est prise en charge.

    Et là, les choses deviennent simple : il y a un endroit, une classe, où sont envoyées toutes les données métiers, quelles qu'elles soient, qui doivent être affichées.

    Les données métiers arrivent à l'entrée et disent "je dois être affichée". Et là, on leur tape un étiquette (police de caractère, textures ou tout ce que tu veux) sur le dos et on les envoies à l'écran. Il faudra peut être passer par quelques astuces, mais ca ne te parrait pas plus simple comme cela

    Et quand bien même tes données métier auraient besoin, qui d'une texture, qui d'une police de caractères, poses toi peut être la question du moment auquel il faut la lui transmettre Au moment de la création. Et la création, c'est typiquement une étape qui doit être centralisée ( patron de conception factory inside) et donc, ca ne fait de nouveau qu'une seule classe qui doit disposer des différents éléments que tu veux mettre dans tes différents gestionnaires.

    Et ta factory, elle ne devrait être utilisée qu'à un seul endroit, elle aussi (vu que, de toutes manières, il faudra bien que tu gères la durée de vie de tes objets quelque part).

    D'une certaine manière, tu dois avoir "tout en haut" d'une pyramide trois classes qui travaillent mains dans la main : une qui décide de créer des objets, une qui crée effectivement et une dernière qui les garde en vie le temps utile et nécessaire. Juste à coté de ces trois classes, il y a tes systèmes son, système de rendu, système IA et peut être quelques autres, mais chacun a son boulot bien défini. Et chacun a besoin de certaines informations dont les autres n'ont strictement que faire.

    Dans la discussion que je pointais, intéresse toi surtout à tout ce qui a trait aux différents modules, à ce qu'ils ont à faire et à ce que ca implique. Elle n'a sans doute rien à voir avec la création d'un jeu video, mais, si tu comprends les principes que j'y énonces, tu peux parfaitement les adapter à ton problème
    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

  8. #8
    Candidat au Club
    Réponse très rapide : ben je suis déjà loin de ce que tu dis en fait. Les données métiers sont dans Element ou Menu. La seule raison d'être de SpriteManager est précisément de prendre ces données métier et de les afficher correctement à l'écran, et c'est (Map mise à part) le seul endroit, la seule classe où je gère l'écran. Element dit à son SpriteManager interne "je dois m'afficher, côté moteur je suis dans tel état à tel endroit, démerde-toi avec ça", et SpriteManager se démerde.

    Donc je suis parfaitement conscient que ni ma Map ni mes Element ni mes Menu n'ont besoin des textures pour fonctionner... Donc tu ne m'en a pas bouché un coin, désolé :p

    Je peux encore séparer plus pour pousser ce que tu dis à l'extrême, mais l'idée est déjà là non ? Le seul "pousser plus loin" que je vois serait de laisser SpriteManager faire ses calculs de "ce qu'il faut afficher à l'écran précisément" et déléguer la partie affichage pure à encore quelqu'un d'autre... qui ne servira presque plus à rien, si ce n'est faire la distinction entre le cas Map et le cas SpriteManager. N'est-ce pas là enrichir le code avec une classe quasi-inutile ?

  9. #9
    Expert éminent
    Je me permet de m'insérer dans la discussion car j'ai un avis différent à apporter. Or il est toujours intéressant d'avoir des avis différent pour trancher.

    Au plus ça va, au moins j'ai d'aversion pour les variables globales. Donc les variables statiques. Donc les singletons. A condition bien sûr de prendre les dispositions nécessaires en fonction du contexte (gestion de la concurrence en contexte multithread, RAII lors de gestion de ressources (sachant que RAII et singleton ne font pas toujours bon ménage), etc.).

    L'idée d'un gestionnaire de ressources qui soit un singleton me parait tout à fait pertinent. A un bémol près: il ne faut pas utiliser le mot gestionnaire (manager), là-dessus je suis d'accord avec Koala. Derrière cette remarque se cache l'idée que ton "manager" ne doit pas tout faire: il faut séparer les différentes tâches inhérentes à la gestion de ressources (chargement, création, organisation/classification, optimisation, accessibilité, etc.) et effectuer un découpage cohérent selon les besoins. Une note importante, qui découle de cela, mais je pense que je ne vous apprendrai rien: il ne faut pas que ton gestionnaire de donnée manipule les données. Par exemple, on peut imaginer un gestionnaire de ressource qui contient des textures et des samples audio. En revanche, il serait suicidaire que ce même gestionnaire soit en charge d'afficher les textures dans les sprites et de lire les samples audio. Le conteneur de ressources doit se contenter de contenir, probablement de libérer, et de proposer les fonctionnalités qui permettent d'accéder à ces ressources. Point barre.

    Après la granularité (niveau de découpage) de tes modules dépend trop du contexte pour pouvoir se prononcer. On ne va pas se lancer dans un MVC en architecture 3 tiers pour implémenter un morpion en ascii par exemple.

    edit: et pour dire un mot sur le sujet initial, si tu souhaites mettre 'singleton' et 'shared_ptr' dans la même phrase, il doit y avoir un 'non' quelque part entre les deux.
    Tester c'est douter, corriger c'est abdiquer.

  10. #10
    Expert éminent sénior
    Et si tu avais quelque chose qui ressemblait à
    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
    /* Ce n'est pas un singleton!!! Controle la durée de vie des différents sprite et
     * permet de récupérer le sprite "qui va bien"
    */
    class SpriteHolder{
       /* ... */
    };
    /* meme chose pour les textures
    */
    class TextureHolder{
       /* ... */
    };
     
    /* ... et pour les polices de caractèrs
    */
    class FontHolder{
       /* ... */
    };
    class View{
    public:
        void draw(Menu const & menu){
            Font fontToUse =fonts_.find(/* ... */);
            render_.show(menu, fontToUse);
     
        }
        void draw(VeryBadBoy const & badboy){
            Texture textureToUse = textures_.find(/* ... */);
             render_.show(badboy, textureToUse);
        }
        /*  ... */
    private:
        FontHolder fonts_;
        TextureHolder textures_;
        SpriteHolder sprites_;
        Render & render_;
    };
     
    int main(){
       Render render(/*...*/); // le moteur d'affichage en lui meme
       View view(render ); // La vue
       GameEngine.game(view); // le moteur de jeu (celui qui demandera à la vue d'afficher les éléments)
       /* ... */
       game.run();
    }
    Où aurais tu besoin d'avoir un singleton dés le moment où tu as correctement partagé les tâches

    Tu n'a qu'un GameEngine qui fonctionne avec une référence sur la vue qui, elle meme utilise les classes qui s'occupent de gérer tout ce qui fini à l'écran et qui fonctionne avec une référence sur le moteur d'affichage en lui-même.

    L'intégrité unitaire se fait d'elle même
    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

  11. #11
    Expert éminent
    Citation Envoyé par koala01 Voir le message
    Où aurais tu besoin d'avoir un singleton dés le moment où tu as correctement partagé les tâches
    Effectivement, je suis d'accord.
    Je dis juste que c'est une autre approche, et que le singleton n'est pas à écarter à priori. Par exemple si j'ai quatre pauvres png et deux mauvais ogg à charger pour mon implémentation révolutionnaire de Pong, bah ma foi, que ce soit un singleton ou des classes savamment articulées, la seule chose qui va changer au final, c'est que j'aurais quelques lignes de code en moins si je choisis le singleton.
    Après évidemment, au plus la complexité augmente, au plus le découpage nécessite d'être affiné, et au plus le choix du singleton va montrer des faiblesses.
    Tester c'est douter, corriger c'est abdiquer.

  12. #12
    Candidat au Club
    @ koala01 : Tes trois classes Holder sont instanciées une seule fois ? Elles sont déclarées où, accessibles d'où ? Parce que finalement, si je comprends bien, ton SpriteHolder a le même rôle que mon SpriteManager (et j'ai plusieurs SpriteManager, un par animation), et ton TextureHolder a le même rôle que mon TextureManager (et lui doit être unique, et peut où non être un singleton... dans ma nouvelle idée c'est une variable statique de la classe SpriteManager et non un singleton accessible de partout). Bref j'ai l'impression qu'on a déjà presque la même approche (même si ce n'était pas le cas au début du fil, et c'est en partie suite à tes réponses ).

    @ r0d : J'aime la nuance que tu apportes Au passage, pour info il s'agit d'un projet de plus grande ampleur que pong, mais pas un mgs non plus c'est plutôt comparable à un Castelvania Symphony of the Night ou un Metroid. Donc ça commence à être assez gros pour réfléchir au design...

  13. #13
    Expert éminent sénior
    Citation Envoyé par r0d Voir le message
    Effectivement, je suis d'accord.
    Je dis juste que c'est une autre approche, et que le singleton n'est pas à écarter à priori. Par exemple si j'ai quatre pauvres png et deux mauvais ogg à charger pour mon implémentation révolutionnaire de Pong, bah ma foi, que ce soit un singleton ou des classes savamment articulées, la seule chose qui va changer au final, c'est que j'aurais quelques lignes de code en moins si je choisis le singleton.
    Après évidemment, au plus la complexité augmente, au plus le découpage nécessite d'être affiné, et au plus le choix du singleton va montrer des faiblesses.
    Le fait est que l'on ne se rend souvent compte que très tard qu'il y a un problème de découpage et que le fait d'essayer de le résoudre va en faire freiner des quatre fers parce que "tu n'imagines pas, on en a pour trois mois".

    Le pire, c'est que le type auras raison, ca va prendre un temps bête pour "quelque chose qui a déjà été fait (payé et que le client ne voudra pas payer une deuxième fois)".

    Alors que faire les chose "correctement" dés le départ n'aurait très certainement pas pris plus longtemps que de le faire " à la rache" mais éviterais, justement, de devoir aller dire au client "désolé, on s'est planté, faut qu'on revoie toute une partie du programme".
    Citation Envoyé par whityranger Voir le message
    @ koala01 : Tes trois classes Holder sont instanciées une seule fois ? Elles sont déclarées où, accessibles d'où ? Parce que finalement, si je comprends bien, ton SpriteHolder
    Elles sont déclarées dans la classe View, en tant que membre privé, et ne sont accessibles de nulle part (seules les fonctions membres de View l'utilisent).

    Et ce sont les fonctions membres de View qui "habillent" les données pour les transmettre au moteur d'affichage.

    De cette manière :
    • il n'y a que View qui doive disposer des fonts, des sprites et autres joyeusetés
    • Commme il n'y a d'office qu'une seule vue, tu t'assure qu'il n'y aura qu'un seul objet qui s'occupe de maintenir les sprites et autres
    • Tes données métiers n'ont plus besoin de s'inquiéter de leur apparence et peuvent se contenter de faire ce pour quoi elles sont prévues.

    Et si tu veux avoir la certitude qu'elles ne seront pas copiées:
    1. déjà, tu ne les transmets à personne
    2. et, pour le cas où tu ferais une bêtise, tu les rend non copiables et non affectables

    a le même rôle que mon SpriteManager (et j'ai plusieurs SpriteManager, un par animation), et ton TextureHolder a le même rôle que mon TextureManager (et lui doit être unique, et peut où non être un singleton... dans ma nouvelle idée c'est une variable statique de la classe SpriteManager et non un singleton accessible de partout). Bref j'ai l'impression qu'on a déjà presque la même approche (même si ce n'était pas le cas au début du fil, et c'est en partie suite à tes réponses ).
    Déjà, tu devrais te méfier de termes "manager", "gérer", etc. Ce sont des termes beaucoup trop vagues qui peuvent à la fois signifier:
    1. créer des objet
    2. manipuler des objets
    3. maintenir des objets et décider de leur vie et de leur mort
    Et ça, ce sont trois responsabilité qu'ils faut garder clairement distinctes


    Ensuite, j'ai envie de dire : mais, à partir du moment où tu as la certitude par construction qu'un variable ne sera jamais copiée et qu'il n'en existera jamais qu'une seule instance (parce qu'il n'existera jamais qu'une seule instance de la classe qui l'utilise, parce qu'il n'y aura jamais ...) pourquoi voudrais tu rendre cette variable globale, au risque de te dire plus tard "mais bon sang, mais c'est bien sur : ma variable est globale, je n'ai qu'à l'exposer"

    Tant qu'un objet est bien au chaud, planqué dans l'accessibilité privée d'un autre qui est le seul à savoir qu'il existe, il a un paix royale et il ne posera jamais le moindre problème (à moins qu'il n'y ait un bug, s'entend )!

    Mais c'est le jour où tu vas commencer à l'exposer "vous avez vu mon bel objet" que les problèmes commenceront à arriver. Surtout si tu travailles en multhread
    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
    Candidat au Club
    Je vais encore te poser 2-3 questions, pour essayer de bien voir ta façon de penser le truc, et ensuite bien réfléchir à tout ça

    Imaginons donc que je n'aie qu'une instance d'une classe View, et qu'une instance de GameEngine qui contiennent respectivement :
    - View : des objets qui gèrent les textures, fonts etc (en séparant avec trois objets gestionnaires pour chacune des responsabilités citées plus haut) ainsi qu'une méthode, appelée par les objets de GameEngine, qui serait un truc style display() et donnant en paramètres le type d'objet et les infos nécessaires (genre je suis un perso de tel type en train de courir depuis 25s et je suis à telles coordonnées de la map, affiche-moi). Du coup pas besoin pour les objets du GameEngine de gérer à la construction ces questions-là, et seule display (et Constructeur, destructeur) est exposée.
    - GameEngine : contiendrait des perso, éléments divers et Map réduits à leurs infos de comportement et de collisions ; posséderait une référence sur View pour appeler la fameuse méthode display() de chacun de ses éléments au moment opportun.
    - un sous-module InputEngine séparé pour gérer les inputs ? Ce qui m'embête là, c'est qu'en grande majorité le résultat des input dépend de la situation du perso principal (par exemple un saut depuis le sol c'est un saut, mais le même input contre un mur c'est un wallJump) et ça un module séparé d'inputs ne devrait pas y avoir accès... auquel cas j'intègre ce module à GameEngine ?

    Si c'est bien ce que tu as en tête, ça me semble assez intelligent... Donc je vais sans doute repenser le truc, mais j'ai un souci : les hitbox sont intimement liées aux actions des personnages, et chargées en même temps (car pour le graphiste il est logique de faire un seul et même travail sur sa planche). Comment je sépare ces infos entre View et GameEngine à la construction ? Avec une factory qui créérait les persos d'une part et enverrait l'info à View ? En lisant deux fois le fichier décrivant ces hitbox/affichages, une fois dans GameEngine et une fois dans View, pour ne récupérer que l'info qui intéresse à chaque étape ?

    Merci d'avance,

    whityranger

    PS : est-ce que je passe le sujet en résolu ? Je pense que c'est bon en ce qui concerne la question du début, mais j'aimerais pouvoir continuer à discuter de ces questions de code design...

  15. #15
    Rédacteur/Modérateur

    Salut,
    Citation Envoyé par whityranger Voir le message
    - un sous-module InputEngine séparé pour gérer les inputs ? Ce qui m'embête là, c'est qu'en grande majorité le résultat des input dépend de la situation du perso principal (par exemple un saut depuis le sol c'est un saut, mais le même input contre un mur c'est un wallJump) et ça un module séparé d'inputs ne devrait pas y avoir accès... auquel cas j'intègre ce module à GameEngine ?
    sur ce point en particulier, tu prends le problème à l'envers.
    Un input, c'est un input. Rien de plus. Une pression de la barre espace ne va pas faire sauter le bonhomme, mais enverra dans le système une pression de la barre espace.
    Si parmi les machins plugés sur le controler manager tu as un bonhomme, il déclenchera son saut, ou autre. Mais l'input est strictement identique.
    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.

  16. #16
    Candidat au Club
    C'est plus ou moins ce que je fais déjà, c'est-à-dire que j'ai une classe KeyConfig qui intercepte les input, et transmet la version "ingame" au personnage ; c'est-à-dire qu'il intercepte la barre d'espace, détermine si c'est "jump" ou "attack" ou autre, et envoie ça au personnage, qui n'a alors pas à savoir si cette requête correspond à une barre d'espace ou un A ou autre chose. La question que je me pose, c'est si cette classe de Config devrait être intégrée à GameEngine ou déclarée en dehors et que GameEngine n'aie qu'une référence dessus, comme pour View.

  17. #17
    Expert éminent sénior
    Citation Envoyé par whityranger Voir le message
    j'ai un souci : les hitbox sont intimement liées aux actions des personnages, et chargées en même temps (car pour le graphiste il est logique de faire un seul et même travail sur sa planche). Comment je sépare ces infos entre View et GameEngine à la construction ? Avec une factory qui créérait les persos d'une part et enverrait l'info à View ? En lisant deux fois le fichier décrivant ces hitbox/affichages, une fois dans GameEngine et une fois dans View, pour ne récupérer que l'info qui intéresse à chaque étape ?
    Ce serait une aberration que de lire deux fois le même fichier

    Tu lis une fois le fichier et tu transmet à qui de droit l'information qui lui convient

    Une solution serait d'avoir une map dont la clé serait un identifiant de l'objet que le volume permet de représenter et dont la valeur serait le volume lui-même.

    Cette map serait intégrée directement dans le moteur de jeu, vu que c'est lui qui devra gérer les correspondances, et supprimer celles qui n'ont plus de raison d'être (idéalement en déléguant quand même tout le travail d'ajout, suppression, recherche à une classe dont ce serait la seule responsabilité), et la vue disposerait d'une référence constante (elle n'a pas à modifier les volumes, juste à les utiliser ) vers cette map / classe spécialisée.

    De cette manière, quand le moteur de jeu doit manipuler des objets pour vérifier les collisions et autres, il peut se contenter de demander le volume de l'objet qu'il doit gérer, sans avoir à s'inquiéter de ce à quoi il correspond réellement.

    De même, le moteur de rendu n'a que faire de savoir si ce qu'il affiche est un menu, un sprite ou un personnage : tout ce qu'il doit savoir, c'est le volume à afficher, la texture à appliquer au volume et la position à laquelle il doit l'afficher.

    Enfin, Lorsque la vue reçoit l'ordre d'afficher un objet, quel qu'il soit, elle peut donc lui demander sa position et son identifiant, rechercher la texture et le volume qui correspondent, et envoyer la position, la texture et le volume au moteur de rendu.

    Encore une fois, tu remarqueras que l'idée est de séparer le plus possible les différentes informations et de veiller à ce que chaque élément ne manipule que le stricte nécessaire

    PS : est-ce que je passe le sujet en résolu ? Je pense que c'est bon en ce qui concerne la question du début, mais j'aimerais pouvoir continuer à discuter de ces questions de code design...
    Ce n'est pas parce qu'un sujet est marqué résolu que l'on ne peut plus continuer la discussion

    Tu peux donc déjà le passer en résolu, comme cela, les gens sauront que tu as eu la réponse que tu attendais

    Pour le reste, on essaye généralement de faire en sorte de "séparer" les différents problèmes, histoire que les discussions puissent servir de base de connaissance.

    Mais il est parfois plus simple d'avoir toutes les informations quand elles se trouvent dans la même discussion

    Mais ne t'en fais pas, si la discussion s'écarte vraiment trop du sujet initial, nous (l'équipe de modération ) pouvons toujours décider de la scinder en plusieurs morceaux
    Citation Envoyé par whityranger Voir le message
    C'est plus ou moins ce que je fais déjà, c'est-à-dire que j'ai une classe KeyConfig qui intercepte les input, et transmet la version "ingame" au personnage ; c'est-à-dire qu'il intercepte la barre d'espace, détermine si c'est "jump" ou "attack" ou autre, et envoie ça au personnage, qui n'a alors pas à savoir si cette requête correspond à une barre d'espace ou un A ou autre chose. La question que je me pose, c'est si cette classe de Config devrait être intégrée à GameEngine ou déclarée en dehors et que GameEngine n'aie qu'une référence dessus, comme pour View.
    En fait, si un élément A doit utiliser un élément B, il faut juste te demander si B devra être effectivement géré par A ou non et si c'est un élément "partagé" ou non.

    Le moteur de jeu n'a absolument pas à s'inquiéter de gérer la vue : il n'en est que l'utilisateur principal (c'est la partie elle-même qui est le "propriétaire légitime" de la vue, et qui peut donc décider d'en créer une autre, pour passer en vue 3D, par exemple).

    Dans de telles conditions, il est "logique" que le moteur de jeu ne dispose que d'une référence vers la vue, tout comme il est "logique" que la vue ne dispose que d'une référence sur la liste des volumes mis à sa disposition (parce que c'est le moteur de jeu qui s'occupera de gérer cette liste de volumes, même s'il délègue certains aspects, et qui en sera donc le propriétaire légitime).

    Poses toi donc simplement la question de savoir qui est le légitime propriétaire de la configuration du clavier. Poses toi la question de savoir s'il y a du sens à faire en sorte que l'utilisateur de cette configuration (le moteur de jeu) n'en soit pas le "propriétaire légitime" (celui qui a toute licence pour la modifier).

    Et tu auras ta réponse : si l'utilisateur d'un objet en est le propriétaire légitime, il est normal qu'il dispose de l'objet en lui-même. Si l'utilisateur "principal" d'un objet n'en est pas le propriétaire légitime, il est normal qu'il ne travaille qu'au travers d'une référence vers cet objet
    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

  18. #18
    Expert éminent
    Citation Envoyé par koala01 Voir le message
    Ce serait une aberration que de lire deux fois le même fichier
    Il y a aussi la solution de ne le lire qu'une fois mais de le garder en mémoire sous forme brute dans une string, et d'aller piocher dedans quand besoin est.
    Après, ça dépend de la taille dudit fichier. Ce ne sera pas non plus une bonne solution si le fichier peut être parsé une fois pour toute (auquel cas ça ne sert à rien de le mettre en mémoire).
    Tester c'est douter, corriger c'est abdiquer.