Forum Programmation.c++ Le constructeur d'une classe de base peut-il savoir qu'il n'est pas le dernier?

Posté par  . Licence CC By‑SA.
-1
10
juil.
2017

Je suis conscient que la question n'est pas très claire, un petit bout de code pour illustrer :

class Base {
  public:
    Base() { this->init(); }
    virtual void init() {}
};

class Derived : public Base {
  public:
    Derived() : Base() { }
    void init() {}
};

class Derived2 : public Base {
  public:
    Derived2() : Base() { this->init(); }
    void init() {}
};


[...]

Base b; // calls Base::init()
Derived d; // calls Base::init()
Derived2 d1: // calls Base::init() and Derived2::init();

Je crois comprendre assez bien les raisons pour lesquelles un constructeur ne peut pas appeler de fonctions virtuelles (parce que ces méthodes virtuelles pourraient tenter d'accéder à des éléments inconnus de la classe de base, par exemple), mais une «solution» pourrait être de savoir d'une manière ou d'une autre dans le constructeur si on est au bout de la chaine d'appel des constructeurs (si on est une «feuille» de l'arbre de la hiérarchie de classes) ou non, ce qui permettrait de faire quelque chose comme if (is_leave()) this->init(); dans tous les constructeurs).

L'autre solution est évidemment de définir à l'avance qui est une classe de base et qui est dérivé, en rendant toutes les classes non-dérivées virtuelles pures, et en dérivant une classe bidon de Base juste pour pouvoir l'instancier.

Il y a une vraie raison derrière ce problème, parce que j'aurais besoin d'initialiser un vecteur dont la taille dépend de la classe. Il y a plein de solutions non-élégantes au problème (redimensionner le vecteur, appeler la fonction virtuelle init() explicitement après le constructeur, …), mais j'ai peut-etre raté quelque chose d'évident?

  • # problème A -> B

    Posté par  . Évalué à 2.

    Je pense que ce que tu cherches à faire est impossible, et j'ai l'intuition que le vecteur dont la taille change selon la classe est une mauvaise solution B à un problème A que j'aimerais bien connaître.

  • # Ordre des constructeurs

    Posté par  . Évalué à 3. Dernière modification le 10 juillet 2017 à 12:40.

    Dans un cas simple (sans héritage multiple ni virtuel), les constructeurs sont appelés dans l’ordre.

    Dans le cas Derived2 :
    D’abord Base::Base() qui s’il appelle init ne peut appeler que Base::init (il ne peut avoir connaissance des autres, et la table des méthodes virtuel pointe pour l’instant sur sa fonction.
    Ensuite Derived::Derived() qui s’il appelle init peut appeler soit la sienne Derived::init() soit celle de Base::init().
    Ainsi de suite.

    Si ton but est que chaque niveau de la hiérarchie appelle sa fonction d’init il n’y a pas de problème.

    Une solution a ton problème pourrait-être d’utiliser une classe de callback sur destructeur.

    class CallBack {
    public:
      CallBack(fct):my_fct(fct) {}
      ~CallBack() {my_fct(/* ancien this ? */);}
    
      modify(fct) { my_fct = fct; }
    private:
      my_fct;
    };

    Je n’ai pas défini le type de fct. On doit pouvoir utiliser std::bind pour inclure le this dans l’appel.

    Ensuite, tu crées ton constructeur un peu comme ça :

    Base::Base (param, callBack(fct)) { callBack.modify(my_init); }

    Le dernier appelé sera celui de plus haut niveau (le dernier exécuté). L’objet temporaire créé pour le passage de paramètre sera détruit à la sortie du constructeur et donc, hors optimisation, le destructeur de l’objet callback devrait être appelé. (je n’ai pas le temps de tester mon idée…).

    Je ne sais pas si ça résous ton problème, mais je rejoins le commentaire au dessus pour dire que ça semble être un soucis d’analyse du problème.

    Par contre, la fonction que tu utilises ne peut être que statique. Ou alors, tu dois enregistrer un pointeur vers ton objet et dans ce cas faire : obj.fct()

    • [^] # Re: Ordre des constructeurs

      Posté par  . Évalué à 2.

      ça semble être un soucis d’analyse du problème.

      Ça pourrait bien être le cas. Ceci dit, il arrive bien souvent qu'un «problème d'analyse» ne soit pas vraiment distinguable d'une chose que C++ ne sait pas faire ; c'est clair que j'essaye de faire quelque chose qui n'est pas naturel en C++. Le principe de ce qui me semblerait naturel (vite fait):

      struct Point {
        Point(double xx, double yy) : x(xx), y(yy) { }
        double x;
        double y;
      };
      
      class Polygon {
        public:
          Polygon() { init(); }
          virtual ~ Polygon() {}
          virtual void init() {
            for (int i = 0; i < get_nb_points(); i++) {
              Point p = Point(cos(i*360/get_nb_points()), sin(i*360/get_nb_points()));
              v.push_back(p);
            }
          }
          virtual int get_nb_points() { return 0; }
        protected:
          std::vector<Point> v;
      };
      
      class Triangle: public Polygon {
        public:
          Triangle(): Polygon() { } 
          int get_nb_points() const { return 3; } // pourrait être plus compliqué, lire dans un fichier, etc. 
      };

      La logique, c'est que Polygon sait se construire tout seul avec un code générique, il lui manque juste quelques infos auxquelles il pourrait accéder par des "getters" virtuels qui, eux, sont spéciques des classes dérivées. Finalement, ça revient à dire que je souhaite juste éviter quelque chose du style Polygon * p = new Triangle(); p->init(); afin d'éviter de pouvoir laisser l'objet dans un état pas complètement construit.

      Le callback permet d'inverser artifciellement l'ordre de la construction, et je réalise que dans tous les cas un bricolage de ce style s'impose. Ça pourrait aussi être une Factory qui s'occuppe d'appeler le constructeur et init() dans la foulée, ou tout un tas d'autres «solutions» (par ex. une fonction statique Polygon* Construct()…). Plus j'y pense et plus j'ai l'impression que ce que j'essaye de faire, c'est de donner également le rôle de Factory à la classe de base, et que ça n'est pas une bonne idée…

      Merci pour les pistes!

      • [^] # Re: Ordre des constructeurs

        Posté par  . Évalué à 3.

        Je comprends un peu mieux ce que tu veux faire… Ce n’est pas possible, car quand tu es dans le constructeur de polygone, le vtable (pointeur vers la table des fonctions virtuelles) est initialisé pour Polygone. Ce n’est qu’à la fin du constructeur de Polygone où le runtime modifie la vtable puis appelle le constructeur de Triangle.

        Dans ce cas simple, la solution est de descendre l’information nb_de_point en paramètre au constructeur de Polygone.

        class Triangle: public Polygon {
          public:
            Triangle(): Polygon(3) { } 
            int get_nb_points() const { return 3; } // pourrait être plus compliqué, lire dans un fichier, etc. 
        };

        Mais sinon, une méthode statique de construction de l’objet peut-être intéressante.

        Pour comprendre l’ordre d’exécution, la vtable… tu peux ajouter des traces (un bête std::cout) et dump(this,sizeof(*this)) dans chaque constructeur. Ça devrait te donner une idée.

        • [^] # Re: Ordre des constructeurs

          Posté par  . Évalué à 2.

          Ce n’est pas possible, car quand tu es dans le constructeur de polygone, le vtable (pointeur vers la table des fonctions virtuelles) est initialisé pour Polygone.

          Oui oui, c'est logique, je l'ai réalisé dès que j'ai repéré un bug lié à ce problème. Le principe de ma hiérarchie de classe est d'utiliser des méthodes génériques de la classe de base, et tout fonctionnait très bien jusqu'à ce que j'essaye de le faire aussi dans le constructeur.

          la solution est de descendre l’information nb_de_point en paramètre au constructeur de Polygone.

          Pour être honnête, j'ai même pensé à passer this en paramètre du constructeur de la classe de base, mais j'ai rapidement décidé que je ne voulais même pas savoir si c'était légal.

          Je suis quand même un peu surpris que le C++ n'ait pas un mécanisme simple pour s'assurer qu'une fonction est exécutée exactement une fois lors de la construction, sans pour autant contraindre la hiérarchie de classes (typiquement, pour réserver de la mémoire). Dès que les classes filles appellent le constructeur des classes mères, on se retrouve à devoir choisir où réserver la mémoire ; soit dans le constructeur de la classe mère (et donc, de devoir se débrouiller pour passer des variables membres en paramètre du constructeur, ce qui est quand même étrange), soit dans le constructeur de la classe fille (ce qui rend presque mécaniquement la classe mère virtuelle). Disons que je comprends le pourquoi du comment techniquement, mais que ça me semble être un mécanisme légitime en POO…

          • [^] # Re: Ordre des constructeurs

            Posté par  . Évalué à 2.

            Personnellement, je m’étais fait avoir sur un destructeur qui appelait une fonction virtuelle pure… ça lance une belle exception ;-).

            Oui oui, c'est logique, je l'ai réalisé dès que j'ai repéré un bug lié à ce problème. Le principe de ma hiérarchie de classe est d'utiliser des méthodes génériques de la classe de base, et tout fonctionnait très bien jusqu'à ce que j'essaye de le faire aussi dans le constructeur.

            Si tu as ce besoin dans le constructeur, c’est que ta hiérarchie est mal répartie. Ce n’est pas un problème de C++, tu aurais le même type d’ennuis dans tous les langages objets que je connais.

            Pour chaque classe tu dois savoir quelles sont ses responsabilités. Avoir les données minimales pour gérer ces responsabilités.

            Si je reprends ton exemple de Polygone, il est logique que le polygone possède la liste des points. C’est même à lui de retourner le nb de points mais en regardant le nombre d’éléments dans la liste.

            int Polygone::get_nb_points() { return v.size(); }

            Par contre positionner les points c’est à la classe fille de le faire… via une nouvelle fonction de Polygone : addPoint.

            Ensuite, je ferais une classe spécifique comme ça :

            template <int n> class PolygoneRegulier: public Polygone 
            {
                PolygoneRegulier():Polygone() {
                   // Ici Polygone est construit, donc on peut utiliser ses méthodes.
                   for (int i = 0; i < n; i++) {
                       Point p = Point(cos(i*360/n), sin(i*360/n));
                       addPoint(p);
                   }
                }
            }
            
            typedef PolygoneRegulier<3> Triangle;
            typedef PolygoneRegulier<4> Carre;

            J’ai tapé ça vite fait… je ne sais même pas si ça compile ;-) mais l’idée est là.

            Pour être honnête, j'ai même pensé à passer this en paramètre du constructeur de la classe de base, mais j'ai rapidement décidé que je ne voulais même pas savoir si c'était légal.

            Ce n’est pas nécessaire, le this est le même dans toute la hiérarchie. Juste qu’une méthode parente n’a pas à appeler une méthode d’une fille, car sinon comment choisir ? Ex :

            class Mere
            {
            public:
                Mere() { Fille::methode(); }
            };
            
            class Fille:public Mere {
                
                void methode();
            };
            
            class Fille2: public Mere {
                
                void methode();
            };
            
            Fille2 var;
            };

            Comment peut-on construire var puisque var sera une Fille2 ainsi qu’une Mere. Mais pas une Fille ! Donc si ça compilait se serait catastrophique.

            Pour la suite je ne suis pas certains d’avoir tous saisi.

            Je suis quand même un peu surpris que le C++ n'ait pas un mécanisme simple pour s'assurer qu'une fonction est exécutée exactement une fois lors de la construction, sans pour autant contraindre la hiérarchie de classes (typiquement, pour réserver de la mémoire).

            Je l’ai expliqué au dessus. Après tu peux faire une construction via une factory.

            Dès que les classes filles appellent le constructeur des classes mères, on se retrouve à devoir choisir où réserver la mémoire ;

            Je ne comprends pas.

            soit dans le constructeur de la classe mère (et donc, de devoir se débrouiller pour passer des variables membres en paramètre du constructeur, ce qui est quand même étrange), soit dans le constructeur de la classe fille (ce qui rend presque mécaniquement la classe mère virtuelle).

            Ton problème vient du fait que tu dois penser en terme de responsabilité :

            • Mon objet c’est quoi ?
            • Quel information caractérise se quoi.
            • Quel comportement je lui donne.

            Si a l’une de ses questions tu dois avoir des méthodes dans des classes qui hériteront c’est qu’il y a un soucis.

            Disons que je comprends le pourquoi du comment techniquement, mais que ça me semble être un mécanisme légitime en POO…

            Je pense que tu découvre la POO et que tu as trouvé génial l’héritage et les fonctions virtuelles, mais il faut bien comprendre se qu’elles permettent et pas leur donner des pouvoirs qu’elles n’ont pas.

            L’exemple classique s’est la figure géométrique que l’on veut déplacer. Il suffit d’appeler la fonction dessiner dans la fonction deplacer. En rendant dessiner virtuelle, ça permet d’avoir une seule implémentation de deplacer.

            Je te conseillerai un livre très bien écrit : Programmer en langage c++ de Claude Delannoy. J’ai une très vieille version 4° édition 1998… il manque le c++03, c++11… néanmoins, la présentation du livre est très didactique et une fois assimilé les concepts de base, les informations glanées sur internet n’ont plus à être aussi didactique.

            Finalement, j’ai fais un paver ! J’espère que ce n’est pas trop indigeste.

  • # Argument du constructeur ?

    Posté par  (site web personnel) . Évalué à 2.

    Pourquoi pas utiliser un constructeur de Base avec argument ?

    #include <iostream>
    #include <typeinfo>
    
    class Base {
    
      public:
    
        Base() { this->init( typeid(this) ); }
    
      protected:
    
        Base( const std::type_info & dti ) { this->init( dti ); }
    
        void init( const std::type_info & dti )
        { std::cout << dti.name() << std::endl; }
    };
    
    class Derived : public Base {
      public:
        Derived() : Base( typeid(this) ) { ; }
    };
    
    class Derived2 : public Base {
      public:
        Derived2() : Base( typeid(this) ) { ; }
    };
    
    int main()
    {
        Base b;
        Derived d;
        Derived2 d1;
    }

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.