Partage
  • Partager sur Facebook
  • Partager sur Twitter

Erreur LNK2019 sur getter si inline uniquement

    17 avril 2022 à 22:41:51

    Bonjour, maitrisant encore mal ce que fais réellement inline et quand l'utiliser, je me retrouve avec une erreur que je ne comprends pas

    Je suis en train de créer une classe abstraite AbstractFighter qui définit quelques comportements de bases communs au héro et aux monstres de mon jeu

    Cette classe à quelques attributs privés et un attribut protégé

    Cette classe fournit des accesseurs en lecture seule sur ces attributs, comme ce sont des fonctions très courtes je les ai inliner

    Cependant j'ai l'erreur du linker LNK2019 qui me dit "symbole externe non résulu" mais uniquement sur le getter de l'attribut protected et uniquement si je le déclare inline

    Les autres getters inline ne posent absolument aucun problème

    // AbstractFighter.h
    
    #pragma once
    
    class AbstractFighter {
    public:
    	AbstractFighter(unsigned int life);
    	virtual ~AbstractFighter() = 0 {};
    
    	inline bool isALive() const noexcept;
    	inline unsigned int maximumLife() const noexcept;
    	inline unsigned int life() const noexcept;
    	inline unsigned int shield() const noexcept;
    
    protected:
    	unsigned int maxLife{ 1 };
    
    private:
    	bool alive{ true };
    	unsigned int currentLife{ 1 };
    	unsigned int currentShield{ 0 };
    };
    // AbstractFighter.cpp
    
    #include "AbstractFighter.h"
    
    AbstractFighter::AbstractFighter(unsigned int life) :
    	maxLife(life),
    	currentLife(life) { }
    
    inline bool AbstractFighter::isALive() const noexcept
    {
    	return alive;
    }
    
    inline unsigned int AbstractFighter::maximumLife() const noexcept
    {
    	return maxLife;
    }
    
    inline unsigned int AbstractFighter::life() const noexcept
    {
    	return currentLife;
    }
    
    inline unsigned int AbstractFighter::shield() const noexcept
    {
    	return currentShield;
    }

    Avec un main tout basique :

    #include <iostream>
    
    #include "AbstractFighter.h"
    #include "Deck.h"
    
    class TestFighter : public AbstractFighter
    {
    public:
    	TestFighter(unsigned int life) : AbstractFighter(life) {}
    };
    
    int main()
    {
        std::cout << "Hello World!\n";
    
        TestFighter fighter{ 80 };
        std::cout << fighter.maximumLife() << std::endl;
    
        return 0;
    }



    Le getter maximumLife produit donc l'erreur de Linker quand il est inline, si je retire inline de la déclaration et de la définition je n'ai plus d'erreur et si je défini la fonction dans le header ça compile sans erreur non plus

    inline unsigned int maximumLife() const noexcept { return maxLife; }  // Pas d'erreur


    Est ce que quelqu'un pourrait m'expliquer pourquoi svp ? J'ai beau mal maitriser et comprendre les concepts autour de inline, rien de ce que je lis ne semble interdire d'inliner un getter sur un attribut protected ... Qu'est ce que je fais mal svp ?


    • Partager sur Facebook
    • Partager sur Twitter
      17 avril 2022 à 23:01:27

      Il y a une erreur de linkage que sur maximumLife() parce que c'est la seule fonction que tu appelles (si tu en appelle d'autres, il te dira aussi qu'il ne les trouve pas).

      Une fonction déclarée inline ne peut être appelée que depuis la même unité de compilation. Quand tu mets directement la définition dans la classe, la fonction est directement inlinée et tout le monde a accès à la définition.

      Le mot clé inline au niveau de la déclaration de la fonction (dans la classe) ne sert à rien, c'est au niveau de la définition qu'il a une utilité, même si tu mets la définition dans la classe (ça sera inline automatiquement). D'ailleurs de nos jours ce mot clé ne sert plus vraiment à dire qu'une fonction est "petite", ça le compilateur le voit, et même si tu met inline, il est quand même libre de ne pas l'inliner. En fait la seule raison de mettre inline c'est si la fonction se retrouve dans plusieurs unités de compilation (par exemple si on la met dans un header qui est inclut à plusieurs endroits), sinon c'est une violation de la One Definition Rule. Et mettre noexcept partout c'est bof

      -
      Edité par JadeSalina 17 avril 2022 à 23:12:20

      • Partager sur Facebook
      • Partager sur Twitter
        17 avril 2022 à 23:03:58

        Oui mon fichier AbstractFighter.cpp est bien compilé

        Je n'ai présenté qu'un main avec le code minimal pour représenter le soucis mais j'ai bien évidemment essayer d’appeler d'autres fonctions de ma classe et elles fonctionnent toutes parfaitement

        Il n'y a vraiment que ce getter maximumLife qui pose problème mais uniquement si j'essaye de l'inliner

        • Partager sur Facebook
        • Partager sur Twitter
          17 avril 2022 à 23:25:44

          J'ai testé ton code, ça compile sans erreur sous Visual C++

          Pas sous MinGw ou Clang à cause du destructeur qui doit être défini en dehors de la déclaration de la classe.

          • Partager sur Facebook
          • Partager sur Twitter
            18 avril 2022 à 2:21:28

            Je commence par le plus simple (et hors sujet) : sauf cas très très exceptionnel, ne fais pas de destructeur virtuel pure. Si tu ne sais pas pourquoi il ne faut pas le faire, c'est que tu ne sais pas quand il faut le faire. Donc ne le fais pas :)

            virtual ~AbstractFighter() = default;

            Pour ton problème de link error, il faut revenir aux bases de la compilation. Quand tu compiles un projet C++, tu as :

            1. chaque fichier .cpp est compilé séparément

            2. les fichiers en-tête sont copier-coller à la place des #include.

            3. les fichiers compilés (un fichier .o pour chaque .cpp) sont linkés ensemble

            (Je mets volontairement de côté la question des modules du C++20 et le LTO "link time optimization").

            Donc par exemple, pour ton fichier AbstractFighter.cpp :

            include "abstractfighter.h"
            
            AbstractFighter::AbstractFighter(unsigned int life)
            ...
            
            inline unsigned int AbstractFighter::maximumLife() const noexcept
            {
                return maxLife;
            }
            
            ...

            Après le passage du pré-compilateur :

            class AbstractFighter {
                ...
                inline unsigned int maximumLife() const noexcept;
                ...
            };
            
            AbstractFighter::AbstractFighter(unsigned int life)
            ...
            
            inline unsigned int AbstractFighter::maximumLife() const noexcept
            {
                return maxLife;
            }
            
            ...
            

            Le inline permet de dire au compilateur que cette fonction sera définie plus loin dans le code. Et c'est ce qu'il se passe effectivement dans le code de AbstractFighter.cpp après le passage du pré-compilateur, la définition est donnée en dessous de la déclaration. Et donc si le compilateur trouve un appel d'une fonction inline, il peut aller chercher la définition de cette fonction et remplacer (s'il a envie) un appel de fonction par du code direct.

            Ok, maintenant, regardons main.cpp. Après le précompilateur, tu as :

            ... iostream 
            
            class AbstractFighter {
                ...
                inline unsigned int maximumLife() const noexcept;
                ...
            };
            
            ...
            
            int main() {
                ...
                std::cout << fighter.maximumLife() << std::endl;
                ...
            }
            

            Dans ce code, la question est où le compilateur trouve la définition de la fonction inline ? Il n'a pas accès à AbstractFighter.cpp à cette étape et donc à la définition des fonctions de AbstractFighter. D'où l'erreur que tu obtiens.

            La doc est explicite :

            Cppreference:

            https://en.cppreference.com/w/cpp/language/inline

            The definition of an inline function or variable (since C++17) must be reachable in the translation unit where it is accessed (not necessarily before the point of access).

            Dit autrement, si tu utilises AbstractFighter::maximumLife() dans main.cpp, la définition de cette fonction doit être accessible dans main.cpp. Dans ton main.cpp, ce n'est pas le cas.

            Pour résoudre le problème :

            - soit tu ajoutes la définition de ta fonction aussi dans main.cpp. (bof)

            - soit tu mets la définition de la fonction inline dans le header (ou un fichier d'implémentation inclue dans le header, comme pour les templates), pour que celle-ci soit accessible partout où elle sera utilisée.

            (Dans les 2 cas, l'ODR "one definition rule" est respectée, puisque c'est inline).

            Et aucun rapport avec protected, le problème se pose pour les autres fonctions aussi. Exemple avec clang :

            Et tu peux mettre noexcept sans problème dans ces fonctions.

            -
            Edité par gbdivers 18 avril 2022 à 2:24:00

            • Partager sur Facebook
            • Partager sur Twitter
              18 avril 2022 à 9:57:47

              Il vaut mieux ne pas mettre inline au niveau de la  déclaration, ça rajoute du bruit dans le code pour rien https://isocpp.org/wiki/faq/inline-functions#where-to-put-inline-keyword

              Et pareil pour noexcept, on peut le mettre bien sûr mais ça apporte pas grand chose et si tout d’un coup on veut renvoyer un std::vector ou autre, surprise, la fonction peut throw. Du coup ça rajoute de la maintenance pour rien. Et si par hasard on appelle une fonction qui peut throw à l’intérieur d’une fonction noexcept, le compilateur va rajouter du code permettant d’appeler std::terminate en cas d’exception alors qu’on a rien demandé. Par contre ça a un intérêt de le mettre sur les move constructor/assignment, la STL peut utiliser cette information à notre avantage.

              • Partager sur Facebook
              • Partager sur Twitter
                18 avril 2022 à 19:33:03

                Bien au contraire, "noexcept" n'est pas un détail d'implémentation et fait partie intégrante du "contrat" de la fonction, exactement comme les "const" des paramètres.

                Le compilateur est ton ami, si vous réfléchissez un peu à la logique de la fonctionnalité.

                • Partager sur Facebook
                • Partager sur Twitter
                Je recherche un CDI/CDD/mission freelance comme Architecte Logiciel/ Expert Technique sur technologies Microsoft.
                  18 avril 2022 à 21:49:56

                  Pour préciser, c'est justement le but de noexcept (et autres fonctionnalités du même ordre : const, concept, typage fort en général, etc) d'ajouter des contraintes sur le code. Le but est si le code ne respecte pas ces contraintes (et donc pourrait avoir un comportement non souhaité), le compilateur nous averti de ce problème.

                  Et bien sûr qu'on peut écrire du code sans utiliser ces fonctionnalités. Mais dans ce cas, les bugs sont détectés plus tardivement (voire rester pendant très longtemps), il faut une couverture de tests plus importantes (voire c'est des clients qui détectent les bugs, ce qui est encore pire), et c'est encore plus difficile a corriger (donc du temps perdu à corriger des bugs au lieu d'implémenter des fonctionnalités).

                  Le bénéfice global d'utiliser ces fonctionnalités est largement en faveur de leur utilisation et c'est bien pour cela que la majorité des devs C++ choisissent de les utiliser (alors que c'est pas une obligation du langage -- sauf cas particuliers -- de les utiliser).

                  JadeSalina a écrit:

                  Il vaut mieux ne pas mettre inline au niveau de la  déclaration, ça rajoute du bruit dans le code pour rien https://isocpp.org/wiki/faq/inline-functions#where-to-put-inline-keyword

                  Dans le cas où la définition est donnée dans le header, comme dans l'exemple donné dans la FAQ, oui.

                  Mais si le choix est de mettre le code source dans tous les codes sources qui utilisent ces fonctions inline (ce qui est une approche très très bof, on sera d'accord sur ce point), alors le bruit d'utiliser inline est a mon avis négligeable, comparé au bruit apporté par le fait de copier-coller le code des fonctions partout.

                  -
                  Edité par gbdivers 19 avril 2022 à 6:01:45

                  • Partager sur Facebook
                  • Partager sur Twitter
                    19 avril 2022 à 10:20:06

                    Ok merci pour vos réponses 

                    Je ne sais pas ce que j'ai bidouillé ce week-end pour avoir l'impression que seule la fonction maximulLife posait problème mais aujourd'hui je confirme bien que toutes mes fonctions inline posent problème si elles sont définis dans AbstractFighter.cpp ce qui est tout à fait logique au vu des explications de Gbdivers

                    Ce que je ne comprend pas du coup sur l'utilisation du inline c'est que :

                    1 - Si je veux inliner une fonction je dois la définir dans dans mon .h (les autres solutions étant "bof")

                    2 - Ce n'est qu'une directive pour le compilateur qui fera bien comme il veut au final

                    3 - Le compilateur va inliner les fonctions qu'il peut inliner même si je ne lui ai pas demandé 

                    4 - Il me semble qu'une fonction définie dans le header sera automatiquement inlinée vous confirmez ?

                    Dans ce cas là, d'un point de vue programmation qu'elle est l'utilité de préciser inline ? Est ce qu'en tant que prorammeur, ajouter inline devant sa fonction ne sert au final qu'à prévenir les autres progammeurs qui viendront lire mon code que cette fonction peut-être inlinée ?

                    Pour répondre à ton hors sujet Gbdivers, en effet je ne sais pas pourquoi il ne faut pas faire de destructeur virtuel pur (je veux bien une explication). En revanche j'ai quand même l'impression qu'un destructeur virtuel pur est adapé à mon cas 

                    AbstractFighter me sert ici d'interface. Cette classe a pour seul but d'implémenter certains comportements de bases communs à tous les combattants

                    Mon hero, les monstres, les boss et les invocations perdent/gagnent des Pv/shield exactement de la même manière. Ils meurent de la même manière quand leurs pv tombent à 0.

                    Ces fonctions n'ont aucune raison d'être spécialisée dans les classes filles je n'ai donc aucune fonction de ma classe AbstractFighter candidate pour être virtuelle pure. Mais celà n'a aucun sens d'instancé un simple Fighter, cette classe doit être abstraite. Il ne reste que la solution du destructeur virtuel pur pour rendre la classe abstaite.

                    Un soucis dans ce raisonnement ?

                    -
                    Edité par ThibaultVnt 19 avril 2022 à 10:20:43

                    • Partager sur Facebook
                    • Partager sur Twitter
                      19 avril 2022 à 12:13:35

                      3- Pas qu'il "peut" mais quand il juge pertinent de le faire, et le choix d'inliner ou pas n'est pas uniquement fonction de la méthode elle-même, mais aussi du contexte d'appel.

                      4- Non, c'est pas automatique.

                      Oui, c'est aussi bien pour le programmeur qui fait la maintenance du code, que pour le compilateur qui peut changer ses paramètres d'évaluation en fonction de la présence ou pas de l'inline, que l'ajout du mot clé "inline" est utile ou pas.

                      • Partager sur Facebook
                      • Partager sur Twitter
                      Je recherche un CDI/CDD/mission freelance comme Architecte Logiciel/ Expert Technique sur technologies Microsoft.
                        19 avril 2022 à 12:35:28

                        1. C'est le contraire.

                        Si tu tiens absolument à ce qu'une fonction soit définie dans un fichier inclus, alors elle doit etre inline (explicitement ou implicitement) (ou constexpr, ce qui est une sorte de cas particulier).

                        2. Relativement à l'inlining même, le compilateur choisira.

                        Mais "inline" (ou implicite dans les déf de classes) reste nécessaire si on veut définir une fonction dans un fichier inclus (retour au 1)

                        Maintenant c'est pour ouvrir la porte à l'inlining que l'on fait 1.

                        3. Dans une UT, le compilo pourra inliner des fonctions dont il voit la définition.

                        Retenir le compilo n'a qu'une règle: celle du "as-if": il doit produire un binaire dont l'exécution observable est conforme au code spécifié, dans la mesure où ce code est correct. Donc. Il inline. Il inline pas. Il fait ce qu'il veut.

                        4. Absolument pas. "inline" n'est implicite que pour les fonctions définies à l'intérieur de définitions de classes. Ce qui permet d'éviter les clashs d'ODR.

                        L'inlining n'a rien à faire de savoir où une fonction est définie. Il faut raisonner en unité de traduction (ou de compilation, meme chose). Le préproc produit une UT, et ensuite le compilo fait ce qu'il veut avec ce qu'il voit tant que: ce qu'il voit est correct, et ce qu'il fait produit un résultat observable conforme -> le as-if.

                        Il y deux notions autour de tout ça:

                        - l'inlining à proprement parler

                        - le mot inline qui permet d'esquiver les clashs de non respect d'ODR.

                        -
                        Edité par lmghs 19 avril 2022 à 14:14:10

                        • 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 avril 2022 à 18:46:14

                          Salut,

                          ThibaultVnt a écrit:

                          4 - Il me semble qu'une fonction définie dans le header sera automatiquement inlinée vous confirmez ?

                          En fait, une fonction sera automatiquement considérée comme "étant déclarée inline" si sa définition (son implémentation) se fait dans la définition de la classe.  Ainsi, si tu as un code proche de

                          class MaClasse{
                              /* ... */
                              /* foo est implémentée directement dans la définition
                               * de la classe
                               */
                              void foo(){
                                  /* ... */
                              }
                          };

                          Le compilateur considérera d'office que la fonction foo a été implicitement déclarée inline.

                          Par contre, si l'implémentation de la classe est "déportée" par rapport à la définition de la classe) (comprend: qu'elle a lieu "nimporte où sauf dans la définition de la classe), par exemple avec un code proche de

                          class MaClasse{
                              /* ... */
                          };
                          
                          void MaClasse::foo(){
                              /* ... */
                          }

                          Alors, le compilateur considérera que si tu avais voulu que foo soit "considérée comme étant inline", tu "n'avais qu'à" le signaler explicitement.

                          EDIT oups, c'est exactement ce que lmghs a dit :p

                          ThibaultVnt a écrit:

                          Pour répondre à ton hors sujet Gbdivers, en effet je ne sais pas pourquoi il ne faut pas faire de destructeur virtuel pur (je veux bien une explication). En revanche j'ai quand même l'impression qu'un destructeur virtuel pur est adapé à mon cas

                          Pour arriver à comprendre pourquoi le destructeur virtuel pur n'est surement pas adapté à ton cas, il faut commencer par comprendre les choses dans l'ordre, à savoir

                          1- Pourquoi déclare-t-on une fonction virtuelle? qu'est ce que cela a comme conséquences?

                          On va déclarer une fonction comme étant virtuelle pour indiquer au compilatur, dans le cadre d'une hiérarchie de classes (faisons simple: en cas d'héritage publique), que le comportement "effectif" de la fonction est susceptible d'être différent, lorsque l'on ne connait "quelque chose" que comme étant "du type de base", en fonction du type réel de ce "quelque chose".

                          Je m'explique:

                          Lorsque tu as recours à l'héritage publique, tu vas travailler avec un code qui pourrait ressembler à quelque chose comme

                          /* voilà la classe de base */
                          class Base{
                               /* je passe la plupart des détails */
                               virtual void foo();
                          };
                          
                          /* et voilà la classe "réellement utilisée", qui hérite
                           * de Base
                           */
                          class Derivee : public Base{
                               /* je passe la plupart des détails */
                               /* on veut définir un comportement particulier 
                                * pour la fonction foo lorsqu'elle est appelée
                                * à partir d'un objet de type Derivee, et ce,
                                * même s'il est connu comme "étant un objet de
                                * type Base"
                                */
                               void foo() override;
                          };
                          /* L'implémentation de ces deux fonctions pourrait 
                           * ressembler à quelque chose comme
                           */
                          void Base::foo(){
                              std::cout<<"Hello from Base\n";
                          }
                          void Derivee::foo(){
                              std::cout<<"Hello from Derivee\n";
                          }

                          L'idée derrière tout cela étant que si j'appelle la fonction foo depuis un objet "connu comme étant de type Base", par exemple, au travers d'un code qui pourrait ressembler à

                          void callFoo(Base & b){
                              b.foo();
                          }

                          je puisse "observer" un comportement différent si je fais appel à la fonction callFoo en lui transmettant un objet qui est effectivement de type Base ou si j'y fait appel en lui transmettant un objet qui est en réalité du type Derivee:

                          int main(){
                              Base laBase; // ca, c'est un objet de type Base
                              callFoo(laBase); // affiche "Hello from Base"
                              Derivee laDerivee; // ca, c'est un objet de type Derivee
                              callFoo(laDerivee); // affiche "Hello from Derivee"
                          };

                          Car c'est le principe même de l'héritage publique:  la fonction callFoo accepte que l'on substitue "n'importe quel type d'objet dérivé de Base" à (la référence sur) l'objet de type Base qu'elle s'attend à recevoir comme paramètre.

                          Je vais passer les détails techniques, sache "simplement" que  le compilateur va mettre en place "toute une mécanique" qui peremttra à callFoo d'appeler la "bonne version" de foo en fonction du type réel de l'objet qu'on lui a transmis ;)

                          2- Pourquoi déclarer qu'une fonction virtuelle est une fonction virtuelle pure?

                          Il faut déjà savoir que ce que l'on appelle généralement le fait de "compiler un projet / un programme" consiste en réalité en deux étapes principales:

                          a- La traduction du code qui se trouve dans les fichiers d'implémentation (les fichiers  *.cpp) dans un langage "compréhensible par la  machine".

                          Cette "traduction" prendra place dans ce que l'on appelle un "fichier objet" que  le compilateur va créer pour nous (c'est -- à  vrai dire -- son seul but dans l'existence :D -

                          b- le "regroupement" de tous les fichiers objets du projet / du programme dans un unique "fichier exécutable" (ton .exe, pourrait-on dire).

                          Cette deuxième étape est prise en charge par un outil appelé "éditeur de liens", qui va ... charger en mémoire le contenu de l'ensemble des fichiers  objets puis qui va "s'amuser", à  chaque fois qu'il remarquera que l'on fait appel à une fonction à faire correspondre l'appel de la fonction avec l'adresse mémoire à laquelle cette fonction se trouve réellement.

                          Seulement, voilà... L'éditeur de liens, il va avoir un mode de fonctionnement exactement identique au compilateur sur un point: s'il ne trouve pas "quelque chose" (comme, au hasard, l'adresse mémoire à  laquelle se trouve le début d'une fonction), il va -- "tout simplement" -- afficher un message d'erreur et abandonner le taf "sur le champs".

                          Autrement dit: toutes les fonctions qui seront appelées lors de l'exécution du programme doivent avoir leur "traduction" dans un des fichiers objets qui auront été générés par le compilateur.

                          Et, par voie de conséquence, il faut que chaque fonction du code C++ que nous écrivons dispose d'une implémentation (ou d'un moyen permettant au compilateur de fournir une traduction "par défaut").

                          Seulement, voilà: pour pouvoir fournir l'implémentation d'une fonction(qu'elle soit fournie "par défaut" par le compilateur ou par nous même), ben, il faut être en mesure de déterminer ce qui pourra être considéré comme un comportement "normal" ou "censé".

                          Dans l'exemple que  je donnais plus haut, je décidais que, quoi qu'il arrive, la fontion foo afficherait quelque chose ("Hello from Base") même (pou plutot "surtout") si le comportement n'était pas redéfini dans les classes dérivées.

                          Ce n'est sans doute pas le "meilleur comportement par défaut" dont on puisse rêver, mais il a au moins le mérite de satisfaire l'éditeur de liens ;)

                          Là où cela va devenir compliqué, c'est lorsque l'on souhaite fournir une "interface" qui ne dispose d'aucune donnée permettant de mettre en place un comportement "par défaut" qui puisse être considéré comme "raisonnable" pour la fonction, parce que si on ne donne pas ce comportement, l'éditeur de liens va râler.

                          On va donc indiquer au compilateur qu'il n'existe pas d'implémentation de la fonction en question, et que c'est voulu en la déclarant ce que l'on appelle "virtuelle pure" à l'aide d'un code  proche de

                          class Base{
                              /* je passe tout le reste */
                              virtual void fonctionVirtuellePure() = 0;
                          };

                          Cette instruction va avoir deux conséquences au niveau du compilateur:

                          Primo, le compilateur va nous interdire de créer une instance de la classe qui contient une fonction virtuelle pure (et de toutes les classes qui en dérivent sans définir de comportement pour cette fonction), parce qu'il s'agirait d'un "objet incomplet" dans le sens où tous les comportements auxquels nous pourrions faire appel ne sont pas présents.

                          Deuxièmement, le compilateur va "laisser un message" à  l'éditeur de liens qui lui dit en substance de ne pas chercher après le début de cette fonction dans la mémoire, parce qu'il ne la trouvera pas.

                          Et comme l'éditeur de liens s'attendra du coup à ne pas trouver le début de cette fonction dans la mémoire, il ne nous fera pas son "caca nerveux" s'il ne la trouve pas ;)

                          Oui, mais du coup, pourquoi  ne devrais-je pas déclarer un destructeur  virtuel pur?

                          D'abord, il faut comprendre que tout objet qui a pu être créé doit pouvoir être détruit.  C'est -- en très gros (et en très simplifié) -- ce que Coplien nous explique avec sa forme canonique, et c'est aussi valable pour les classes qui existent au sein d'une hiérarchie de classe ;)

                          Tu constateras donc tout de suite que c'est en fait une très mauvaise idée d'indiquer au compilateur "qu'il n'y a pas d'implémentation du destructeur, et que c'est voulu" ;)

                          Car cela implique, dans une hiérarchie de classes, que tu peux créer la partie correspondant à la classe de base pour toutes les classes qui en dérivent, mais que tu ne pourras plus détruire cette partie lorsque tu n'en as plus besoin.

                          Bien sur, il y a toujours moyen de fournir une implémentation pour une fonction virtuelle pure, cependant, tu avoueras que ca laisse l'impression d'avoir affaire à  quelqu'un d'un peu tordu sur les bords... Le genre de type qui te dit blanc et qui fait noir ;)

                          On va donc éviter -- si possible -- d'avoir recours à la déclaration du destructeur comme étant virtuel pur, tout  en fournissant son implémentation ;)

                          Mais alors, comment faire?

                          Lorsque l'on fait hériter une classe d'une autre, nous avons donc deux possibilités pour faire face à  la destruction d'un objet de la classe dérivée

                          • Ou bien, nous décidons que l'objet en question (qui est du type de la classe dérivée) ne peut être détruit que s'il est connu comme étant "de son type réel"
                          • Ou bien nous décidons que l'objet en question (qui est du type de la classe dérivée) peut également être détruit s'il est connu comme étant "du type de base".

                          Autrement dit, si nous avons -- encore une fois -- une hiérarchie de classes "basique" proche de

                          class Base{
                              // peu importe les détails 
                          };
                          class Derivee : public Base{
                              // peu importe les détails
                          }

                          et que je crée un objet de type dérivé mais qui serait connu comme "étant du type de base" sous une forme proche de

                          int main(){
                              Base * laDerivee = new Derivee;
                          }

                          La question à  se poser est : puis-je "sans danger" détruire l'objet pointé par  laDerivee (en appelant delete dessus) alors que je ne le connais dans mon code que comme étant du type ... Base ?

                          Si oui, il va falloir prévenir le compilateur que le comportement du destructeur de Base est "susceptible de s'adapter au type réel" de nimporte quelle classe dérivée (en le déclarant comme virtuel et dans l'accessibilité publique), sinon, il va falloir emêcher quiconque ne disposant que d'un pointeur  sur un objet de type Base d'appeler delete dessus, tout en permettant au type Derivee de faire appel au destructeur de Base.

                          Comment? hé bien, en déclarant le destructeur de Base comme étant "non virtuel" (vu que  son comportement ne doit plus être adapté) et dans l'accessibilité protégée (pour que le destructeur de Derivee puisse y faire appel automatiquement).

                          Au final, nous avons donc deux possibilités d'écrire notre code, en fonction de la réponse qui sera donnée à cette fameuse question de

                          uis-je "sans danger" détruire l'objet pointé par laDerivee (en appelant delete dessus) alors que je ne le connais dans mon code que comme étant du type ... Base ?

                          Dans le premier cas, nous aurons un code qui ressemblera à quelque chose comme

                          class Base{
                          public:
                              /* je passe le reste */
                              /* le comportement du destructeur doit s'adapter
                               * au type réel de l'objet qui est détruit
                               */
                              virtual ~Base();
                          };
                          class Derivee : public Base{
                              /* je passe le reste */
                          };

                          et, dans le second cas, nous serions alors plutôt face à un code proche de

                          class Base{
                              /* je passe le reste */
                          protected:
                              /* le destructeur de la classe n'est accessible qu'aux classes 
                               * qui en dérivent
                               * il n'y a donc pas de raison de faire en sorte que son
                               * comportement s'adapte
                               */
                          
                              ~Base();
                          };
                          class Derivee : public Base{
                              // je passe le reste
                          };
                          • Partager sur Facebook
                          • Partager sur Twitter
                          Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                            21 avril 2022 à 10:33:40

                            Merci pour ta réponse très détaillée Koala. J'ai pris mon temps pour répondre pour pouvoir le relire plusieurs fois, la tête bien fraiche pour tout saisir 

                            J'avais déjà compris le mécanisme des fonctions virtuelles et des virtuelles pures dans une hiérarchie de classe mais je n'avais en effet pas mesuré l'impact que ça pouvait avoir de déclarer le constructeur comme une fonction virtuelle pure. Ton explication à ce propos était très clair 

                            En revanche je ne vois pas comment appliquer tes explications générales à mon cas précis.

                            AbstractFighter doit être une classe abstraite, ça n'aurait aucun sens de pouvoir instancé un Fighter.

                            Mais les 6 fonctions publiques qu'implémente ma classe AbstractFighter ne seront pas spécialisées par les classes dérivées. Ce sont 6 comportements de bases, très simple et 100% identique pour toutes les classes qui vont dérivées d'AbstractFighter. Je n'ai donc aucune fonction d'AbstractFighter candidate pour être virtuelle pure.

                            Dans le cas où aucune de mes fonctions publiques ne peuvent etre virtuelle pure, si il ne faut pas déclarer le constructeur comme virtuel pur alors quelle est la solution pour rendre ma classe AbstractFighter abstraite ?

                            La seule idée qui me traverse l'esprit serait quelque chose du genre :

                            class AbstractFighter
                            {
                            public:
                                AbstractFighter(/* détails */) {};
                                
                                // Diverses fonctions publiques non virtuelles
                            
                            protected:
                                virtual void useless_Pure_Virtual() = 0;
                            };
                            
                            
                            // ---- Classes dérivées ---
                            
                            class Hero : public AbstractFighter
                            {
                            public:
                                Hero(/* Details*/) : AbstractFighter() {};
                            
                            protected:
                                void useless_Pure_Virtual() override {}; // Implémentation vide
                            };
                            
                            class FriendlyInvocation : public AbstractFighter
                            {
                            public:
                                FriendlyInvocation(/* Details*/) : AbstractFighter() {};
                            
                            protected:
                                void useless_Pure_Virtual() override {}; // Implémentation vide
                            };
                            
                            class Monster : public AbstractFighter
                            {
                            public:
                                Monster(/* Details*/) : AbstractFighter() {};
                            
                            protected:
                                void useless_Pure_Virtual() override {}; // Implémentation vide
                            };

                            Ce qui permet effectivement à la classe AbstractFighter d'être abstraite et non instanciable dans le reste du code sans polluer l'interface publique des classes dérivées mais ça oblige à fournir une implémentation vide de la fonction useless_Pure_Virtual pour chaque classe qui dériverait d'AbstractFighter mais bon .... implémenter des trus inutiles et cachés dans le protected pour que ça marche comme prévu ... ça semble être un choix de conception particulièrement mauvais 

                            • Partager sur Facebook
                            • Partager sur Twitter
                              21 avril 2022 à 12:42:09

                              Il y a fort fort longtemps, j'avais partagé ma vision sur la modélisation de jdr. La version courte. On ne veut pas d'héritage pour tous les types de héros & cie, mais des traits. Une seule classe pour la créature et des ensembles de traits qui vont altérer les résistances, les capacités, etc. Pareil pour le centre de décision qui est déporté, etc.

                              ( https://openclassrooms.com/forum/sujet/bug-d-heritage-16831#r2671749  il y a eu d'autres discussions plus pertinentes et perdues dans les limbes du temps...)

                              Depuis, on a commencé à parler d'ECS. C'est un peu pareil, mais mieux réfléchi, plus abouti, etc.

                              -
                              Edité par lmghs 21 avril 2022 à 12:43: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.
                                21 avril 2022 à 16:59:01

                                ThibaultVnt a écrit:

                                En revanche je ne vois pas comment appliquer tes explications générales à mon cas précis.

                                Héhé... C'est parce que tu ne t'es pas encore posé toutes les  questions intéressantes, et, accessoirement, que  tu n'as pas encore le réflexe d'explorer toutes les possibilités.

                                Mais ne t'en fais pas: ca viendra ;)

                                Mais reprenons l'histoire de ta classe AbstractFighter depuis le début.

                                Tu t'es de toute évidence posé une question intéressante qui t'a mené déterminer que

                                il n'y a absolument aucun sens à  permettre la création d'une instance de AbstractFighter "sans autre précision"

                                La question était tout à fait bonne et sensée, de même que la conclusion que tu en as tirée

                                Par contre, la décision que tu as prise à partir de cette conclusion, elle, elle laisse vraiment "à désirer"

                                <digression mode = "on">Mais, au fait, tu t'es posé la question de la création, mais, qu'en est il de la destruction? Y a-t-il le moindre sens à permettre la destruction d'un objet "passant pour être" un AbstractFighter?</digression>

                                Car, tu sembles oublier, c'est que le fait que l'on ne puisse pas créé d'instance d'une classe contenant une fonction virtuelle pure n'est qu'un "effet secondaire", un "effet de bord" lié à l'absence de l'implémentation de cette fonction. Ou du moins, confonds "simplement" la cause et la conséquence:

                                C'est parce que l'on a décidé de ne pas donner d'implémentation pour la fonction virtuelle pure que le compilateur décide en réaction d'interdire la création d'une instance de la classe.

                                La logique qui se baserait sur le fait que "je vais décider volontairement de ne pas fournir d'implémentation d'une fonction (en la déclarant virtuelle pure) pour que le compilateur m'en interdise la création d'une instance" est ... plutôt tordue, non?

                                Parce qu'il faut quand même se dire que la création d'un objet est prise en charge par un mécanisme tout  à fait spécifique, qui est représenté dans notre code par ... le constructeur.

                                Et dés lors, pourquoi faudrait il dépendre d'une décision du compilateur (qui n'est que l'outil que  l'on utilise), qui sera -- de plus -- basée sur des éléments qui n'ont rien à  voir avec le constructeur (la présence ou non de fonction virtuelles pure) pour faire appliquer une décision conceptuelle et qui ne dépend donc que de nous?

                                Il y a peut-être une solution plus facile et "moins dépendante du compilateur" pour obtenir un résultat similaire? non? allons, réfléchis un peu... Tu  ne vois vraiment pas?

                                Et que penses tu de l'idée de réduire l'accès au constructeur en le plaçant -- par exemple -- dans l'accessibilité protégée, histoire que les classes dérivées puissent y avoir accès ?

                                Ne serait-ce pas suffisant pour garantir l'ensemble de nos "obligations"?

                                Mieux encore: Tu es d'accord avec moi que la "logique" d'utilisation des données est sensiblement "toujours la même":

                                1. on crée (une instance de) la donnée
                                2. on la manipule selon les besoins
                                3. on la détruit lorsqu'elle devient inutile

                                "Créer, manipuler, détruire", voilà les trois "étapes primordiales" du développeur informatique, sa "sainte trinité" pourrait-on dire.  Bien sur, les termes peuvent varier ("Initialiser, utiliser, finaliser", par exemple) pour s'adapter aux situations, cependant on en reviendra toujours à ces trois points ;)

                                Hé bien, tu pourrais parfaitement demander à  trois personnes différentes et n'ayant aucun contact entre elles de prendre chacune de ces étapes en charge, à condition bien sur de leur donner "suffisamment d'informations" sur la manière dont ces étapes doivent être envisagées.

                                Par exemple, tu pourrais demander à  Henry

                                Peux tu  me faire un constructeur  pour la classe AbstractFighter (qui prenne tels et tels paramètres) et qui soit en mesure de garantir que seules les classes dérivées pourront être créées (peu  importe le nombre de ces classes dérivées)?

                                Puis tu pourrais  aller voir Marc et tu  lui dire:

                                Peux tu me fournir une série de fonctions pour la classe AbstractFighter qui permettront de <une liste de questions que l'on veut poser et d'ordre que l'on veut donner> dont le comportement puisse (ou non) s'adapter et pour lesquelles il faut (ou non) prévoir un comportement par défaut?

                                Et, enfin, tu  pourrais aller voir Luc et lui demander

                                Peux tu  me fournir un destructeur pour la classe AbstractFighter qui permette (ou non) de détruire un objet de la classe dérivée?

                                ou même

                                Peux tu me fournir un destructeur pour la classe AbstractFighter qui interdise la destruction d'un objet "passant pour en être un"?

                                Sais tu que, une fois que ces trois personnes t'auront remis leur code, du devrais pouvoir "rassembler le tout" et obtenir une classe AbstractFighter utilisable (pour autant que tous les besoins aient été clairement expliqués ;) )

                                 Comment cela, "ca ne tient pas debout"?  Mais si ca tient debout... C'est même l'une des manière de travailler utilisée lorsque le "secret défense" est en jeu: on ne transmet aux "exécutant" que juste assez d'informations que pour leur permettre de faire ce qui leur est demandé ;)

                                ThibaultVnt a écrit:

                                Mais les 6 fonctions publiques qu'implémente ma classe AbstractFighter ne seront pas spécialisées par les classes dérivées. Ce sont 6 comportements de bases, très simple et 100% identique pour toutes les classes qui vont dérivées d'AbstractFighter. Je n'ai donc aucune fonction d'AbstractFighter candidate pour être virtuelle pure.

                                C'est exactement le but de cette intervention: te faire comprendre qu'il y a d'autre moyens que les fonctions virtuelles pures pour éviter l'instanciation d'une classe :D

                                ThibaultVnt a écrit:

                                La seule idée qui me traverse l'esprit serait quelque chose du genre :

                                class AbstractFighter
                                {
                                public:
                                    AbstractFighter(/* détails */) {};
                                    
                                    // Diverses fonctions publiques non virtuelles
                                
                                protected:
                                    virtual void useless_Pure_Virtual() = 0;
                                };
                                
                                
                                // ---- Classes dérivées ---
                                
                                class Hero : public AbstractFighter
                                {
                                public:
                                    Hero(/* Details*/) : AbstractFighter() {};
                                
                                protected:
                                    void useless_Pure_Virtual() override {}; // Implémentation vide
                                };
                                
                                class FriendlyInvocation : public AbstractFighter
                                {
                                public:
                                    FriendlyInvocation(/* Details*/) : AbstractFighter() {};
                                
                                protected:
                                    void useless_Pure_Virtual() override {}; // Implémentation vide
                                };
                                
                                class Monster : public AbstractFighter
                                {
                                public:
                                    Monster(/* Details*/) : AbstractFighter() {};
                                
                                protected:
                                    void useless_Pure_Virtual() override {}; // Implémentation vide
                                };

                                Ce qui permet effectivement à la classe AbstractFighter d'être abstraite et non instanciable dans le reste du code sans polluer l'interface publique des classes dérivées mais ça oblige à fournir une implémentation vide de la fonction useless_Pure_Virtual pour chaque classe qui dériverait d'AbstractFighter mais bon .... implémenter des trus inutiles et cachés dans le protected pour que ça marche comme prévu ... ça semble être un choix de conception particulièrement mauvais 

                                Et dans ce cas particulier, tu as tout à fait raison: avoir recours à une fonction virtuelle pure "planquée" dans l'accessibilité protégée, avec pour obligation de la part des classes dérivée de réimplémenter cette fonction (qui  ne fait rien, en plus) serait effectivement un très mauvais choix de conception.

                                Mais si c'est la seule idée qui te vienne à  l'esprit, c'est parce que c'est la seule solution que tu aies apprise jusqu'à présent.  Ou peut être parce que tu  penses à tort que le constructeur et (ou ) le destructeur ne peuvent se trouver que dans l'accessibilité publique.

                                Or, au cas où les choses ne seraient pas claires, il n'y a rien qui t'interdise de placer un constructeur ou un destructeur dans l'accessibilité qui "convient le mieux" aux restrictions que l'on veut imposer à la classe, si bien qu'une hiérarchie de classes qui ressemblerait à

                                class AbstractFighter
                                {
                                public:
                                    
                                    // Diverses fonctions publiques non virtuelles
                                
                                protected:
                                    
                                    AbstractFighter(/* détails */) {};
                                    ~AbstractFighter(){/* détails */} // il pourrait être virtuel et publique ... ou non
                                };
                                
                                
                                // ---- Classes dérivées ---
                                
                                class Hero : public AbstractFighter
                                {
                                public:
                                    Hero(/* Details*/) : AbstractFighter() {};
                                
                                protected:
                                };
                                class FriendlyInvocation : public AbstractFighter
                                {
                                public:
                                    FriendlyInvocation(/* Details*/) : AbstractFighter() {};
                                
                                protected:
                                  
                                };
                                
                                class Monster : public AbstractFighter
                                {
                                public:
                                    Monster(/* Details*/) : AbstractFighter() {};
                                
                                protected:
                                  
                                };

                                présentera l'ensemble des conditions requises ;)

                                -
                                Edité par koala01 21 avril 2022 à 17:02:35

                                • Partager sur Facebook
                                • Partager sur Twitter
                                Ce qui se conçoit bien s'énonce clairement. Et les mots pour le dire viennent aisément.Mon nouveau livre : Coder efficacement - Bonnes pratiques et erreurs  à éviter (en C++)Avant de faire ce que tu ne pourras défaire, penses à tout ce que tu ne pourras plus faire une fois que tu l'auras fait
                                  21 avril 2022 à 17:55:54

                                  lmghs a écrit:

                                  Il y a fort fort longtemps, j'avais partagé ma vision sur la modélisation de jdr. La version courte. On ne veut pas d'héritage pour tous les types de héros & cie, mais des traits. Une seule classe pour la créature et des ensembles de traits qui vont altérer les résistances, les capacités, etc. Pareil pour le centre de décision qui est déporté, etc.

                                  ( https://openclassrooms.com/forum/sujet/bug-d-heritage-16831#r2671749  il y a eu d'autres discussions plus pertinentes et perdues dans les limbes du temps...)

                                  Depuis, on a commencé à parler d'ECS. C'est un peu pareil, mais mieux réfléchi, plus abouti, etc.

                                  -
                                  Edité par lmghs il y a environ 5 heures


                                  C’est la meilleure chose à faire, plus personne ne fait d’héritage pour dire qu’on a des types d’ennemis. Comment faire si on a envie d’ajouter des capacités à un Ennemi (Peut être une idée de gameplay, à la Kirby par exemple) si la structure même du code impose les capacités? Une hiérarchie de classes de ce style est un mauvais design dans le cas présent
                                  • Partager sur Facebook
                                  • Partager sur Twitter

                                  Erreur LNK2019 sur getter si inline uniquement

                                  × 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