Partage
  • Partager sur Facebook
  • Partager sur Twitter

Le destructeur doit toujours être virtuel ?

    24 janvier 2024 à 23:34:17

    Salut tout le monde !

    J'étais en train de lire Professional C++ (5th edition) de Marc Gregoire lorsque j'ai vu qu'il conseillais de toujours déclarer les destructeurs comme virtuels.

    Pourtant ça me paraît être une aberration... Pour moi, il me paraît logique que le destructeur doit être virtuel si et seulement si la classe est prévue pour être une base classe. Le destructeur de la base classe doit être virtuel notamment pour prévoir la destruction correcte de la derived classe dans ce cas de figure :

    std::unique_ptr<Base> ptr = std::make_unique<Derived>();

    Mais sinon c'est inutile et néfaste d'un point de vue performance non ?

    Pour l'instant j'utilise la façon de faire de Effective C++: 55 Specific Ways to Improve Your Programs and Designs (3rd edition) de Scott Meyers qui est de ne définir le destructeur comme virtuel uniquement lorsque la classe est héritée par des classes filles. Scott Meyers dit même que définir tous les destructeurs comme virtuels peut entraîner des problèmes de performance non-négligeables. Exemple : une petite classe de 2 int qui représente donc 64 bits en mémoire risque de faire 128 bits si on ajoute un destructeur virtuel et donc une vtable. Cela peut empêcher les instances de la classe d'être stockées dans les registres si la classe dépasse 64 bits et empêcher la portabilité avec d'autres langages comme le langage C qui ne possède pas le keyword virtuel.

    Effective C++ est cependant un vieux livre... Il est sorti en 2005 et peut-être plus d'actualité avec le standard actuel.

    Je suis aussi allé sur stackoverflow pour essayer de tirer ça au clair mais la plupart des topics datent d'avant le C++11... et à l'époque on dirait que les discussions tournaient principalement autour de l'absence d'un keyword final (qui existait déjà en Java) pour empêcher les classes avec des destructeurs non-virtuels d'être héritées... J'aimerais bien lancer un topic pour discuter de ce qu'il en est sur stackoverflow depuis l'arrivée du C++11 mais je suis question-banned depuis longtemps et même avec un nouveau pseudo, mon topic sera traité en tant que duplicate et je serai renvoyé vers les vieilles discussions pré-C++11.

    Un expert ici pour débunker cette histoire ? Qui a raison entre Marc Gregoire et Scott Meyers ? :pirate:

    PS : je suis preneur si vous avez des recommandations de bons livres de C++ qui sont à la page (je connais déjà les basiques C++ Primer et Effective Modern C++)

    -
    Edité par ThomasAirain 24 janvier 2024 à 23:40:31

    • Partager sur Facebook
    • Partager sur Twitter
      24 janvier 2024 à 23:53:08

      ThomasAirain a écrit:

      J'étais en train de lire Professional C++ (5th edition) de Marc Gregoire lorsque j'ai vu qu'il conseillais de toujours déclarer les destructeurs comme virtuels.

      Tu as vu ça où dans le livre ?

      EDIT: ok, trouvé (je pense)

      ThomasAirain a écrit:

      The Need for virtual Destructors (p. 348)

      Destructors should almost always be virtual. Making your destructors non-virtual can easily result in situations in which memory is not freed by object destruction. Only for a class that is marked as final could you make its destructor non-virtual.

      Globalement, c'est la même idée : soit la classe est dérivable et le destructeur doit être virtuel, soit la classe ne l'est pas et devrait être final. Final n'existait pas encore a l'époque de Effective C++.

      -
      Edité par gbdivers 25 janvier 2024 à 0:02:08

      • Partager sur Facebook
      • Partager sur Twitter
        25 janvier 2024 à 0:01:53

        Dans :

        Chapitre 10 : Discovering Inheritance Techniques

          BUILDING CLASSES WITH INHERITANCE

            The Need for virtual Destructors

        2 citations :

        "Destructors should almost always be virtual. Making your destructors non-virtual can easily result in situations in which memory is not freed by object destruction. Only for a class that is marked as final could you make its destructor non-virtual."

        "WARNING : Unless you have a specific reason not to or the class is marked as final, destructors should be marked as virtual."

        On dirait qu'il ne dit pas explicitement de toujours mettre le destructeur comme virtuel >_< mais il incite à mettre le destructeur quasiment toujours virtuel.

        -
        Edité par ThomasAirain 25 janvier 2024 à 0:03:55

        • Partager sur Facebook
        • Partager sur Twitter
          25 janvier 2024 à 0:02:30

          ThomasAirain a écrit:

          The Need for virtual Destructors (p. 348)

          Destructors should almost always be virtual. Making your destructors non-virtual can easily result in situations in which memory is not freed by object destruction. Only for a class that is marked as final could you make its destructor non-virtual.

          Globalement, c'est la même idée : soit la classe est dérivable et le destructeur doit être virtuel, soit la classe ne l'est pas et devrait être final. Final n'existait pas encore a l'époque de Effective C++.

          • Partager sur Facebook
          • Partager sur Twitter
            25 janvier 2024 à 0:05:25

            Du coup y a que ces 2 possibilités pour l'usage courant du C++ : destructeur virtuel ou class final

            Pourtant je ne vois quasiment jamais le keyword final utilisé...

            • Partager sur Facebook
            • Partager sur Twitter
              25 janvier 2024 à 0:09:56

              Et on voit beaucoup de pointeurs nus en C++...

              C'est la vie du C++.

              • Partager sur Facebook
              • Partager sur Twitter
                25 janvier 2024 à 0:49:11

                Bonjour,

                Les 2 ne s'adressent pas au même public.

                Marc Gregoire dit à quelqu'un qui débute: si vous de voulez pas avoir de problèmes (de mauvaise destruction), utilisez un destructeur virtuel.
                Scott Meyers, est lui un expert qui s'adresse à des personnes averties. Il précise que c'est parfois non nécessaire, voire contre-productif.

                Un destructeur protected, est suffisant dans certains cas.
                Un destructeur public et non virtual peut même parfois être suffisant. Mais il faut alors prendre d'autres précautions, ça devient vraiment "osé".

                Aucun risque, car on n'héritera jamais de cette classe:

                struct Base final {
                };

                Pas de risque (a priori, voir plus bas), on ne pourra pas détruire la classe Derivee dans un contexte où elle est référencée comme Base

                struct Base {
                protected: ~Toto() = default; };

                Plus osé, si la Derivee est toujours créée via un std::shared_ptr<Derivee>:

                struct Base {
                  //protected: ~Base() = default;
                };
                struct Derivee : public Base {
                };
                std::shared_ptr<Base>  ptsh_base = std::make_shared<Derivee>();  // OK
                std::unique_ptr<Base>  ptun_base = std::make_unique<Derivee>();  // Aïe

                Le shared_ptr<> est porteur du destructeur de Derivee, mais l'unique_ptr<> lui va appeler le destructeur de Base! Il faut a minima indiquer le destructeur de Base comme protected, l'unique_ptr<> ne compile alors plus, et le shared_ptr<> reste utilisable.

                Et pour les 2 derniers exemples, si on ajoute un friend à Base, il pourra alors détruire une Base qui est en fait une Derivee, et on aura alors un problème.

                Donc la réponse de Marc Gregoire est la plus sécuritaire, Celle de Scott Meyers nécessite une lucidité supplémentaire.

                • Partager sur Facebook
                • Partager sur Twitter
                  25 janvier 2024 à 1:33:25

                  Un exemple de classe dérivable, non final et sans destructeur virtuel, c'est boost::noncopyable https://github.com/boostorg/core/blob/develop/include/boost/core/noncopyable.hpp

                  Ici, le destructeur est protected, pour éviter les problèmes indiqué par Dalfab. 

                  • Partager sur Facebook
                  • Partager sur Twitter
                    25 janvier 2024 à 2:20:54

                    Systématiser les destructeurs virtuels est assez ridicule en ce qui me concerne -- je range ça dans la même catégorie que définir toutes les fonctions comme virtuelles "parce que on ne sait pas où on va". Le dtr virtuel répond à un besoin bien précis. Si dans le doute il faut une règle simplificatrice: "s'il y a un virtuel quelques part => destructeur public et virtuel (ou protégé et non-virtuel)". Et aussi penser à activer les warnings!

                    Ce que je systématise en revanche, c'est l'interdiction de copie dès qu'il y a un virtual.

                    Dès que je vois des classes copiables avec un destructeur virtuel, si c'est sur du code que je maintiens, je nettoie toujours. Copiabilité ou virtualité, il n'en restera qu'un.

                    En règle post 2011, je rajouterai qu'à défaut d'écrire ses propres capsules RAII (ou des scoped_logger), un bon destructeur est defaulté, ou non indiqué.

                    Quant à final, je m'étais posé la question de flagguer ainsi toutes mes classes valeurs. Seul problème, cela interdit l'import de code via héritage privé. Qui lui a parfois du sens. Du coup, mes classes valeur ne sont jamais finales. Et donc clairement, je ne suis pas d'accord avec la recommandation simplifiée de l'auteur.

                    • 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.
                      3 février 2024 à 23:23:56

                      @lmghs Je pense pareil que toi. Pour moi, le destructeur ne doit pas toujours être virtuel sauf dans le cas d'une base classe où là cela a du sens car il y aura la présence de l'héritage.

                      Je viens de relire C++ Primer, qui est souvent considéré comme la référence loin devant les autres du C++, et il est beaucoup plus évasif que Professional C++ sur la question.

                      C++ Primer indique clairement que le destructeur doit être virtuel lorsqu'il y a héritage. Donc cela est admis.

                      En revanche il n'indique pas les exceptions à cette règle. Il indique qu'une base class doit "généralement" définir un destructeur virtuel. Donc C++ Primer insinue que ce n'est pas une règle qui est toujours à appliquer (du coup ça aurait pu être bien d'indiquer QUAND le destructeur ne doit pas être virtuel mais bon passons).

                      Quand au mot clé final, C++ Primer dit que le programmeur PEUT l'utiliser mais il ne recommande pas spécialement, ni n'oblige spécialement le programmeur à l'utiliser.

                      En gros, je trouve ce que dit C++ Primer est tout à fait juste mais le livre ne se mouille pas trop. C++ Primer dit qu'il faut utiliser le destructeur virtuel et le mot clé final lorsque cela est opportun mais cela est sujet à interprétation d'où l'éclosion des débats sur ce sujet.

                      -
                      Edité par ThomasAirain 3 février 2024 à 23:27:06

                      • Partager sur Facebook
                      • Partager sur Twitter
                        4 février 2024 à 4:11:07

                        C++ Primer est un cours.

                        Ce genre de considérations avec règles et étude des exceptions à la règle, tu les trouveras ailleurs. Typiquement, dans le /C++ Coding Style/ de Sutter & Alexandrescu, qui a atterri dans les C++ Core Guidelines.

                        Après, "le virtual ou copie il faut choisir", il est présent en filigrane dans les C++ Core Guideline, mais c'est surtout dans la communauté francophone qu'on le rabâche.

                        Les recommandations dans les ouvrages anglophones ça sera juste: "si pas besoin de polymorphisme dynamique, alors pas de virtual". Et encore, ce n'est pas une évidence/un réflexe pour tous avec les choix de certains langages (Java, Python...). Pire, à une époque j'ai vu le "foutons des virtuels partout, ça sera plus simple pour la suite".

                        Et malheureusement les justifications techniques récurrentes de "ça sera plus rapide" (-> https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c10-prefer-concrete-types-over-class-hierarchies ), c'est très léger et n'aborde pas un seul instant les aspects conceptuels.

                        • 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.

                        Le destructeur doit toujours être virtuel ?

                        × 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