Partage
  • Partager sur Facebook
  • Partager sur Twitter

std::any induit une double suppression

    19 novembre 2021 à 13:27:50

    Bonjour. 

    Je suis face au problème suivant.

    #include <iostream>
    #include <any>
    
    
    class Ptrsurint
    {
    public:
    	int* n;
    	Ptrsurint(int m_n) {
    		n = &m_n;
    	}
    	~Ptrsurint() {
    		std::cout << *n << "\n";
    	}
    };
    
    
    
    int main()
    {
        Ptrsurint pt(3);
        std::any a = pt;
    }
    

    Après l'éxecution de ce code, la sortie sur la console est

    209430
    209445

    Ce qui prouve que le destructeur de Ptrsurint est appelé sur l'objet pt ET sur l'objet a. Cependant, dans un certain projet, j'utilise std::any sans pouvoir me permettre de détruire deux fois l'objet sans générer d'erreur. Comment puis-je dans l'exemple plus haut ne pas détruire le pointeur n deux fois (un peu comme si j'utilisais un pointeur void* à la place de any)?


    • Partager sur Facebook
    • Partager sur Twitter
    Le basheur
      19 novembre 2021 à 13:35:55

      Une double libération, ça se voit généralement avec un message explicite (sous les linux du moins). Dans le doute compiler avec l'address sanitizer.

      Sinon, ce code précis (je ne peux pas dire pour le code avant simplification) est très certainement moisi. m_n prend l'adresse d'une variable locale à la fonction qui l'affecte. (et oui, les paramètres sont des variables locales un peu particulières).

      En plus, ta classe va très certainement etre manipulée par valeur et dupliquée -- signature (2), avec copie https://en.cppreference.com/w/cpp/utility/any/any . m_n contiendra non seulement l'adresse d'une variable locale périmée, mais en plus, celle qui vient de la construction d'un autre objet.

      Et autant dire que lorsque tu déréférences, l'adresse mémoire ne t'appartient plus.

      => s'il y a une erreur ici, elle n'est très certainement pas dans any. C'est rare qu'il y ait ce genre de bugs évidents dans les implémentations des libs standards. Ce ne sont pas ces implémentations qu'il faut commencer par remettre en question, mais nous m^eme. Si, si.

      EDIT: je soupçonne que dans le vrai code (puisque tu pares de double destruction), c'est toi même qui allouait le pointeur à la main et comme là, piège de non-initié (/mauvais cours), tu n'as défini aucune des 2 opération de copie, ce qui fait qu'il devrait être interdit de dupliquer ta classe. Bref, la solution sécure: arrêter avec les allocs à la main => unique_ptr/make_unique
      Au moins tu aurais vu que le compilo ne te laissait pas (à juste titre) mettre ton objet dans un any de la sorte.

      -
      Edité par lmghs 19 novembre 2021 à 14:25:40

      • Partager sur Facebook
      • Partager sur Twitter
      C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
        19 novembre 2021 à 16:14:59

        D'accord pour le caractère pourri de cet exemple, le code sert d'illustration.

        Je voyais dans mon code de base la double itération par un message explicite, je n'ai donné qu'une illustration.

        Je suis d'accord avec la sûreté du code de any dans la lib.

        J'ai en effet oublié d'interdire l'une des deux formes de copies, merci pour la remarque !

        Je ne suis par contre pas friands de la solution "arrête les allocs à la main". Je n'avais pas le choix (j'ai utilisé DirectX, qui est bas niveau). Cependant, je reconnais ne pas avoir été assez précis. J'utilise std:any pour pouvoir passer un objet en paramètre à une méthode de manière anonyme, ce qui aide à régler certaines questions d'héritage sans utiliser void*. Mais je veux le passer sans devoir le copier quand je le convertit en std::any. Avec ça, je me rends compte que c'est absurde. Le passage par les pointeurs est-il inévitable?

        • Partager sur Facebook
        • Partager sur Twitter
        Le basheur
          19 novembre 2021 à 16:56:22

          BelhouariSéhane a écrit:

          Je ne suis par contre pas friands de la solution "arrête les allocs à la main". 

          Si tu veux faire de la gestion de ressource a la main, il faut au moins encapsuler dans une classe, pour faire du RAII.

          class MyObject {
              MyObject() { directx_allocate(); }
              ~MyObject() { directx_free(); }
          };

          (ou avec un deleter dans un unique_ptr/shared_ptr)

          En soi, le problème n'est pas de faire à la main, juste que faire a la main est beaucoup plus compliqué et risqué et que s'il existe une solution simple et safe (le RAII), ca serait dommage de ne pas l'utiliser.

          BelhouariSéhane a écrit:

          J'utilise std:any pour pouvoir passer un objet en paramètre à une méthode de manière anonyme, ce qui aide à régler certaines questions d'héritage sans utiliser void*. Mais je veux le passer sans devoir le copier quand je le convertit en std::any. Avec ça, je me rends compte que c'est absurde. Le passage par les pointeurs est-il inévitable?

          any impose que cela soit copiable. Copie et héritage, ce ne fait pas bon ménage. C'est assez étrange ce que tu veux faire. Il faudrait nous donner une meilleure idée de ton but.

          • Partager sur Facebook
          • Partager sur Twitter
            20 novembre 2021 à 20:05:25

            En faite, j'ai une classe mère JeuIC. Plusieurs classes différentes en héritent. Il y a dans JeuIC le prototype suivant:

            class JeuIC{
            ...
            public:
            virtual void aff(donnees sur l'affichage)=0;
            ...
            }

            Les données permettent de customiser l'affichage, et leur format varie grandement selon la classe (fille) qui appelle aff. Par exemple, une classe fille A peut demander deux couleurs, alors qu'une classe fille B nécessite une liste de bitmap (l'objet dont j'avais mal géré la copie). Il faut donc une classe DataA pour les données nécessaires à A (deux couleurs), et DataB pour B (la liste de bitmap).

            J'ai voulu gérer les différents types de données d'affichage par any:

            class JeuIC{
            ...
            public:
            virtual void aff(std::any data)=0;
            ...
            }


            Mais du coup je dois copier l'objet DataB lorsque je le convertit en any pour le passer en paramètres. Ce n'est donc pas la bonne solution. Une serait de passer un pointeur au lieu de l'objet lui même, mais j'ai l'impression qu'utiliser any n'est pas la solution la plus propre. 

            -
            Edité par BelhouariSéhane 20 novembre 2021 à 20:06:44

            • Partager sur Facebook
            • Partager sur Twitter
            Le basheur
              20 novembre 2021 à 22:34:51

              L'utilité du polymorphisme, c'est de pouvoir écrire un code de façon indépendante du type réel d'un objet.

              void crier(Animal* p) {
                  p->pousserCri(); // pas besoin de savoir si p est
                                   // un pointeur sur Chien ou Chat
              }
              
              crier(new Chien);
              crier(new Chat);

              A partir du moment où tu casses cela (c'est a dire que tu as un design qui oblige à connaître le type réel, par exemple avec dynamic_cast), tu perds l'intérêt du polymorphisme. Et c'est donc souvent le signe d'un problème de design.

              Avec any en paramètre de fonction, tu perds le polymorphisme. C'est souvent une erreur que l'on voit chez les débutants, qui veulent mettre des trucs qui n'ont pas de liens ensemble dans une même collection. Avec any, tu devras ecrire un truc comme ca :

              class A : base {
                  void aff(std::any) override; // un double en arg
              };
              
              class B : base {
                  void aff(std::any) override; // deux doubles en arg
              };
              
              void foo(base* p) {
                  p->foo(???); // faut-il passer 1 ou 2 doubles ?
              }

              Lors de l'appel de la fonction, le fait d'utiliser std::any ne supprime pas la question de savoir quelles données appeler la fonction. C'est impossible sans a un moment donné casser le polymorphisme.

              Je ne sais pas quel est le bon design dans ton cas, sans avoir plus d'infos. Regarde du côté des implémentations de scenegraph, c'est un problème classique. (Et les fonctions virtuelles sont aussi un problème en général, pour des raisons de performances)

              • Partager sur Facebook
              • Partager sur Twitter
                21 novembre 2021 à 0:39:27

                Mon design est sûrement le problème. Je vais présenter mon problème.

                J'ai une class JeuIC qui corresponds à un jeu à information complète à 2 joueurs (Ex: le jeu d'échecs, de dames, ...). La classe n'a que des méthodes virtuelles, comme coups_legaux(int cote) qui retourne la liste des coups légaux à partir d'une position. 

                Je peux alors créer une classe Echecs qui hérite de JeuIC. Elle contiendra le plateau de jeu sous forme de matrice, et les méthodes comme

                coups_legaux(int cote) override;


                .

                L'avantage est que l'algorithme de jeu (alphabeta) ne dépends que des fonctions virtuelles de JeuIC, donc est implémenté pour tout les jeux. 

                C'est là qu'apparait la difficulté. Pour deux jeux, un coup est une classe différente. Aux echecs, c'est deux positions, alors qu'au dames, c'est une liste de positions. Il faut donc une classe Coup pour chaque jeu. 

                Je dois passer un coup en paramètre à la fonction jouer de JeuIC,

                 virtual void jouer(Coup cp) =0 


                ce qui est impossible puisque le type coup dépends du jeu choisi. Pourtant, JeuIC ne dois pas traiter les coups, juste les véhiculer.

                En passant les coups du jeu d'échecs sous forme de std::any, j'évite les problèmes de type et peux reconvertir le any en Coup dans la fonction .

                Echecs::Jouer(std::any cp);



                Mais ça ne semble pas être la meilleure solution. Vous remarquez alors que les classes virtuelles posent en général un problème d'optimisation. Comment vous y prendriez vous ?

                -
                Edité par BelhouariSéhane 21 novembre 2021 à 0:42:32

                • Partager sur Facebook
                • Partager sur Twitter
                Le basheur
                  21 novembre 2021 à 3:29:15

                  Il semble que tu as oublié le principe de substituabilité de Liskov.

                  Que ce soit les echecs et les dames, les pieces ont une chose en commun:
                  Le droit de se déplacer sur une case parmis une liste, la liste est fonction du type concret de la piece.

                  Perso, je ferais quelque chose du genre:

                  class Box
                  {
                  private:
                      std::pair<unsigned, unsigned> mPosition;
                  public:
                      Box(unsigned x, unsigned y);
                      std::pair<unsigned, unsigned> getPosition() const;
                  };
                  
                  class Piece
                  {
                  public:
                      enum Color
                      {
                          Black,
                          White
                      }
                  private:
                      Box mPosition;
                      virtual std::vector<Box> getAuthorizedBoxes() const;
                  public:
                      Piece(Piece::Color color, Box const& position);
                      Piece(Piece const&) = delete;
                      Piece& operator=(Piece const&) = delete;
                      virtual bool canMoveTo(Box const& box) const;
                      virtual void moveTo(Box const& box);
                  };
                  
                  class King:
                      Public Piece
                  {
                  private:
                      virtual std::vector<Box> getAuthorizedBoxes() const override;
                  public:
                      virtual bool canMoveTo(Box const& box) const override;
                      virtual void moveTo(Box const& box) override;
                  };
                  
                  // idem pour les 5 pieces restantes

                  A charge de ta fonction Jouer, de jongler correctement avec les fonctions membre canMoveTo() et MoveTo().

                  Echecs::Jouer(Piece& piece);

                  -
                  Edité par Deedolith 21 novembre 2021 à 3:30:45

                  • Partager sur Facebook
                  • Partager sur Twitter
                    21 novembre 2021 à 4:01:55

                    BelhouariSéhane a écrit:

                    Vous remarquez alors que les classes virtuelles posent en général un problème d'optimisation. Comment vous y prendriez vous ?

                    Comme tu parlais d'affichage dans ton précédent code, j'ai supposé que c'était pour faire du rendu. Pour cela que j'ai parlé de scenegraph. Et dans ce contexte, les performances sont en général critiques.

                    Mais dans ton cas, ce n'est pas forcément un problème. Ca peut en être un si tu fais beaucoup de calculs, mais difficile à dire pour le moment.

                    Cependant, tu n'as pas réellement besoin de polymorphisme, dans le sens où tu ne vas pas manipuler des objets sans que leur nature ne soit déterminée. Dit autrement, tu ne vas jamais manipuler dans ton JeuIC des pièces d'échec et des pièces de dames.

                    Pour être concret, regarde l'utilisation de dynamic_cast dans ta situation (dynamic_cast qui est, comme je l'ai dit plus tôt, un problème pour le polymorphisme) :

                    // Base
                    class Position { ... };
                    
                    class Piece { 
                        virtual void bougePiece(Position* p);
                    };
                    
                    // chess
                    class ChessPosition : Position { ... }
                    
                    class ChessPiece : Piece {
                       void bougePiece(Position* p) override;
                    };
                    
                    // dames
                    class DamesPosition : Position { ... }
                    
                    class DamesPiece : Piece {
                       void bougePiece(Position* p) override;
                    };
                    

                    Quand tu vas écrire le code de "ChessPiece:: bougePiece", tu vas devoir récupérer le type concret de "Position". Par exemple :

                    void ChessPiece::bougePiece(Position* p) {
                        ChessPosition* cp = ???
                        ...
                    }

                    Mais en fait, dans ce code, à aucun moment tu ne peux avoir autre chose que "ChessPosition". Ca n'aurait aucun sens d'avoir un "DamesPosition". Ton objet "Position" a l'apparence d'un objet polymorphique, mais c'est plus une tromperie syntaxique. Et dans cette situation, tu n'aurais pas besoin de "dynamic_cast" mais juste d'un "static_cast".

                    Ce code ne brise pas le polymorphisme en vrai.

                    Quand on a fait cette analyse que le polymorphisme n'est pas un besoin dans ce contexte, il est possible d'envisager d'autres designs. Perso, je pense a une classe template (JeuIC<Chess> et JeuIC<Dames> serait alors 2 types différents, ce qui correspond plus au fait que ca n'a pas de sens de mixer 2 jeux ensemble).

                    Pour la même raison, std::any (qui est une forme de polymorphisme, mais encore moins contraint qu'avec l'héritage) est encore moins adapté.

                    BelhouariSéhane a écrit:

                    J'ai une class JeuIC qui corresponds à un jeu à information complète à 2 joueurs (Ex: le jeu d'échecs, de dames, ...). 

                    C'est probablement la première critique que je ferais : trop vouloir en faire. C'est une erreur classique de vouloir un code qui fait des choses complètement différentes, sans relations entre elles. C'est possible, mais cela implique beaucoup plus de boulot au niveau du design (voire que l'on arrive a un design complètement moisi, parce que définitivement pas compatible).

                    BelhouariSéhane a écrit:

                    La classe n'a que des méthodes virtuelles, comme coups_legaux(int cote) qui retourne la liste des coups légaux à partir d'une position. 

                    Dans la même lignée de "trop vouloir en faire", c'est d'avoir une classe qui fait tout. On appelle ça un "god object". Ca peut sembler une bonne idée, parce que tout est rassemblé ensemble, mais cela limite très fortement les possibilités de conception, parce que tu ne peux pas découper ton code en termes de concepts abstraits de jeux (des pieces, des mouvements, des prises, des coups legaux ou non legaux, etc).

                    BelhouariSéhane a écrit:

                    C'est là qu'apparait la difficulté. Pour deux jeux, un coup est une classe différente. Aux echecs, c'est deux positions, alors qu'au dames, c'est une liste de positions. Il faut donc une classe Coup pour chaque jeu. 

                    C'est a mon avis là que le bas blesse : tu veux appliquer un concept très abstrait (un algo de graph) sur un design qui n'est pas une abstraction correcte.

                    Si tu veux écrire un algo alphabeta générique, alors tu dois t'affranchir des notions de coups par exemple. Tu as juste des notions de "node", "level", etc.

                    Si tu veux au contraire faire une abstraction des jeux, tu ne peux pas ne pas définir les concepts abstrait de coups, pieces, etc. Et avoir une réalisation concrete de ces concepts pour chaque jeu. (Comme j'ai montré dans mon premier code, avec les classe "Piece", "ChessPiece", etc).

                    Dans ton code, tu utilises std::any uniquement parce que tu n'as pas défini correctement ces concepts. Et tu t'affranchi de faire cette définition correcte en utilisant un type qui veut dire "je représente n'importe quoi". Ce qui est faux, c'est pas n'importe quoi, c'est des concepts concret lorsque tu es dans le code de chaque jeu particulier.

                    ------------------------------------------------------

                    Au final, mon conseil est (si tu n'as jamais fait ca) d'écrire le code pour un jeu en particulier (chess par exemple). Pas de commencer par le code d'un programme qui peut résoudre n'importe quel jeu. Et d'écrire ton algo dans un premier temps sur un jeu en particulier.

                    Le probleme de la conception, c'est qu'il faut avoir des bonnes notions dans les techniques de design (les patterns, les approches, etc) et dans le domaine que tu veux abstraire (même pour un simple jeu d'échec, c'est pas si simple de définir les concepts abstraits de piece, mouvement, regles, etc).

                    Partir dans un premier temps sur une telle conception, ca sera forcément la merde (sauf si tu es un dev tres tres expérimenté). Il est souvent beaucoup plus efficace de partir d'un cas particulier et de généraliser ensuite.

                    Rien que pour un simple jeu d'échec, il est possible de faire plusieurs conceptions différentes. Aucune n'étant meilleure que les autres. J'ai proposé 4 syntaxes differentes l'autre jour sur discord pour un jeu d'echec.

                    -
                    Edité par gbdivers 21 novembre 2021 à 4:11:53

                    • Partager sur Facebook
                    • Partager sur Twitter
                      21 novembre 2021 à 11:39:46

                      "Que ce soit les echecs et les dames, les pieces ont une chose en commun:

                      Le droit de se déplacer sur une case parmis une liste, la liste est fonction du type concret de la piece."

                      Je trouve que cela pose problème de trop modifier l'algorithme pour ce genre de raisons. La complexité serait d'ailleurs moins bonne de cette manière. Mais il semble y avoir une alternative à l'héritage. 

                      "Quand on a fait cette analyse que le polymorphisme n'est pas un besoin dans ce contexte, il est possible d'envisager d'autres designs. Perso, je pense a une classe template (JeuIC<Chess> et JeuIC<Dames> serait alors 2 types différents, ce qui correspond plus au fait que ca n'a pas de sens de mixer 2 jeux ensemble)."

                      Je pense que la solution template est en effet meilleure. J'écrirais alors un truc du genre

                      alphabeta<Echecs>(...)

                      et Echecs pourra déjà contenir les méthodes coups_legaux,... et une classe interne Echecs::Coup.

                      "Au final, mon conseil est (si tu n'as jamais fait ca) d'écrire le code pour un jeu en particulier (chess par exemple). Pas de commencer par le code d'un programme qui peut résoudre n'importe quel jeu. Et d'écrire ton algo dans un premier temps sur un jeu en particulier."

                      Je comprends qu'il soit plus simple de le faire pour commencer un jeu en particulier. C'est ce que j'avais fait pour les échecs(donc je pars bien d'un cas particulier pour généraliser), mais le jeu d'échecs n'est pas super pour le débogage des algorithmes de recherche arborescente (il est très compliqué). C'est pourquoi je préfère coder l'alphabeta (et ses extensions) sur un jeu plus simple, mais de manière générique. De plus, la généralité du code donne vraiment beaucoup de possibilités.

                      -
                      Edité par BelhouariSéhane 21 novembre 2021 à 12:39:13

                      • Partager sur Facebook
                      • Partager sur Twitter
                      Le basheur

                      std::any induit une double suppression

                      × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                      × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                      • Editeur
                      • Markdown