IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

Appel d'une méthode virtuelle dans un constructeur


Sujet :

C++

  1. #1
    Membre habitué Avatar de Rodrigue
    Inscrit en
    Août 2002
    Messages
    487
    Détails du profil
    Informations forums :
    Inscription : Août 2002
    Messages : 487
    Points : 157
    Points
    157
    Par défaut Appel d'une méthode virtuelle dans un constructeur
    Bonsoir,

    Je souhaiterais construire différents objets générants automatiquement des tableaux contenants des coordonnées de points (je simplifie).

    Pour ce faire, j'ai créé une classe mère:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    class MainA
    {
    public:
       MainA(int number_of_point); //constructeur
    protected:
       virtual void Generate(/* ...*/)=0; //fct virtuelle responsable de la génération des différents points
       std::vector< std::pair<float, float> > array; //tableau de points
    };
    et des classes filles, dont en voici une pour l'exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ExpA
    {
    public:
       ExpA(int number_of_point): MainA(number_of_point) {;}
    protected:
       void Generate(/* ...*/) {
          array.pushback(std::pair<float,float>(0.0f,3.14f);
          //...
       }
    dans laquelle je définis la fonction virtuelle générale.

    Le problème c'est que j'appelle la fonction virtuelle Generate depuis le constructeur de la classe mère...
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    MainA::MainA(int number_of_point) {
       Generate(/* ... */);
    }
    Pour moi ça ne pose pas de problème (bon je sais qu'il y en a un vu que ça crash )
    La classe mère appelle sa fonction virtuelle au lieu d'appeller la fonction surchargée/définie de la classe fille:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    ExpA a(10); //boom! Appel d'une fonction virtuelle
    Il va de soi que je n'instancie jamais d'objet MainA vu qu'il s'agit d'une classe abstraite.

    Je sais que la solution consiste à déplacer l'appel de la fonction Generate dans le constructeur de la classe fille (je suis fainénant ça m'énerve de devoir le réécrire à chaque fois)... mais bon je me demande quant même pourquoi il n'appelle pas la fonction surchargée ... (oui c'est parce qu'il est à l'intérieur de l'autre constructeur -> je trouve que ça ne fonctionne pas si bien que ça: on doit dupliquer plein de code).

    Alors là, je me dis qu'une fonction de callback ça fonctionnerait qd même bien mais bon niveau simplicité y'a mieux!

    Bonne soirée!
    Rodrigue

  2. #2
    Membre expérimenté

    Profil pro
    Inscrit en
    Juin 2006
    Messages
    1 294
    Détails du profil
    Informations personnelles :
    Localisation : Royaume-Uni

    Informations forums :
    Inscription : Juin 2006
    Messages : 1 294
    Points : 1 543
    Points
    1 543
    Par défaut
    Salut,

    Citation Envoyé par Rodrigue Voir le message
    je me demande quant même pourquoi il n'appelle pas la fonction surchargée ...
    Parce qu'au moment de l'appel l'objet de type ExpA n'existe pas encore.

    Sinon il y a visiblement un problème de design, c'est le cas quand on rencontre ce genre de situation, en plus ici tu as des données protégées ce qui est un autre code smell un peu dans le même style...
    Par exemple pourquoi pas plutôt une méthode MainA::Add pour ajouter les valeurs et appeler Generate (non virtuel du coup) depuis le constructeur de ExpA ?

    MAT.

  3. #3
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Salut,

    Le constructeur fonctionne selon un ordre bien précis:
    • Appel du constructeur de la classe mère, s'il y en a une
    • Initialisation des membres dans l'ordre de leur déclaration
    • appel des fonctions se trouvant dans le constructeur.

    Evidemment, si tu parle d'un arbre d'héritage, tu entre dans une logique de "poupées russes": le constructeur de chaque classe appelant celui de la classe dont il hérite.

    Seulement, voilà...

    Si une classe fille sait très bien quelle est sa classe mère, une classe mère n'a aucun moyen de savoir quel est le type final de l'objet :

    Ce peut être, simplement, la classe "en cours" (s'il ce n'est pas une classe abstraite) ou chacun des types qui en héritent, directement ou indirectement.

    Du coup, tout appel à une méthode au sein du constructeur ne peut appliquer que les méthodes:
    • définies au sein du type en cours de traîtement
    • déjà définies au niveau de la classe mère

    Fatalement, quand tu est au niveau de la racine de ton arbre d'héritage, et que tu appelle une méthode qui est alors virtuelle pure, la méthode... n'est pas implémentée, et le pointeur la concernant dans la vtable pointe sur l'adresse... 0 (ce n'est en définitive que la définition "de base" d'une méthode virtuelle pure )

    Toute tentative d'appel à cette fonction ne peut que se solder par... un crash magistral

    En outre, si tu utilise un arbre d'héritage à plusieurs niveaux, dont seule la classe racine est abstraite, tu risque de te trouver dans une situation complexe à gérer.

    Pour t'en convaincre, voici un code exemple:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
     
    class A
    {
        public:
            A(){}
            virtual~A(){}
            virtual void Function()=0;
        /*...*/
    };
    class B:public A
    {
        public:
            B(){}
            virtual ~B(){}
            virtual void Function{/* do something for B type */}
    };
    class C:public B
    {
        public:
            C(){}
            virtual ~C(){}
            virtual void Function{/* do something for C type */}
    };
    int main()
    {
        C obj;//appel du constructeur de C
    }
    Tu peux souhaiter que le constructeur appelle function, en espérant qu'elle fasse ce qui est applicable au type réel.

    Pour B, il n'y a pas de problème: Apres avoir appelé le constructeur de A et initialisé ses propres membres, on peut faire appel à Function, et tout roule

    Par contre, pour C, c'est tout à fait différent:

    Son constructeur appellera systématiquement celui de B... et provoquera l'appel de la version de Function sous son implémentation pour B, avant d'initialiser les membre de C, et d'effectuer ce qui se trouve dans le constructeur de C (qui peut lui-même, pourquoi pas, appeler la version de Function qui est propre à C).

    Si c'est ce que tu attends comme fonctionnement, ben, tu es tout à fait libre de travailler ainsi, mais il n'y a aucun moyen de faire que seule la version propre à B ne soit appelée que on a bel et bien affaire à un objet de type B et que la version propre à C soit appelée si l'objet est de type C... Du moins, par appel dans le constructeur
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  4. #4
    Membre habitué Avatar de Rodrigue
    Inscrit en
    Août 2002
    Messages
    487
    Détails du profil
    Informations forums :
    Inscription : Août 2002
    Messages : 487
    Points : 157
    Points
    157
    Par défaut
    Citation Envoyé par Mat007 Voir le message
    Parce qu'au moment de l'appel l'objet de type ExpA n'existe pas encore.

    Sinon il y a visiblement un problème de design, c'est le cas quand on rencontre ce genre de situation, en plus ici tu as des données protégées ce qui est un autre code smell un peu dans le même style...
    Par exemple pourquoi pas plutôt une méthode MainA::Add pour ajouter les valeurs et appeler Generate (non virtuel du coup) depuis le constructeur de ExpA ?MAT.
    Oui, je sais mais comme je l'avais indiqué à la fin du message je suis fainéant
    Citation Envoyé par Rodrigue
    Je sais que la solution consiste à déplacer l'appel de la fonction Generate dans le constructeur de la classe fille (je suis fainénant ça m'énerve de devoir le réécrire à chaque fois)...
    Sinon tu me conseilles quoi comme autre design?

    J'ai une classe qui doit générer un tableau d'éléments en un coup (pas deux appels):
    Le seul paramètre à lui passer est un entier représentant la taille du tableau (un std::vector).

    J'ai bien évidemment plusieurs fonctions différents... A chaque fois la seule chose qui change, c'est la méthode Function. Tu vas me dire que je pourrais mettre toutes ces fonctions dans la classe mère, elle même et en dériver d'autres classes filles qui passerait seulement un "switch"... Me fais-je bien comprendre?

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    enum SuperSwitch {F1, F2, F3}
    class MainA
    {
    public:
    	MainA(int n, SuperSwitch choice) 
    		, _choice(choice)
    	{
    		data.resize(n);
     
    		if(! Generate())
    			throw;
    	}
     
    private:
    	SuperSwitch _choice;
    	std::vector<float> data;
     
    	bool Generate(void)
    	{
    		switch(choice)
    		{
    		case F1:	for(int i=0; i<data.size(); i++)	data[i] = F1(i);	break;
    		case F2:	for(int i=0; i<data.size(); i++)	data[i] = F1(i);	break;
    		case F3:	for(int i=0; i<data.size(); i++)	data[i] = F1(i);	break;
     
    		default: return false
    		}
     
    		return true;
    	}
     
    	float F1(float x)	{	return /* ... */;}
    	float F2(float x)	{	return /* ... */;}
    	float F3(float x)	{	return /* ... */;}
    };
     
    class MainF1: public MainA
    {
    public:
    	MainF1(int n) : MainA(n, F1) {;}
    };
     
    class MainF2: public MainA
    {
    public:
    	MainF2(int n) : MainA(n, F2) {;}
    };
     
    class MainF3: public MainA
    {
    public:
    	MainF3(int n) : MainA(n, F3) {;}
    };
    Rodrigue

  5. #5
    Membre habitué Avatar de Rodrigue
    Inscrit en
    Août 2002
    Messages
    487
    Détails du profil
    Informations forums :
    Inscription : Août 2002
    Messages : 487
    Points : 157
    Points
    157
    Par défaut
    Citation Envoyé par koala01 Voir le message
    Salut,
    Si une classe fille sait très bien quelle est sa classe mère, une classe mère n'a aucun moyen de savoir quel est le type final de l'objet :

    Ce peut être, simplement, la classe "en cours" (s'il ce n'est pas une classe abstraite) ou chacun des types qui en héritent, directement ou indirectement.

    Du coup, tout appel à une méthode au sein du constructeur ne peut appliquer que les méthodes:
    définies au sein du type en cours de traîtement
    déjà définies au niveau de la classe mère
    Merci pour ton explication très claire !

    Et toi comment t'y prendrais-tu pour implémenter ce que je souhaite faire?
    Qu'est-ce qu'y serait le plus propre? Utiliser des pointeurs de fontion (fonction de callback) ça peut être élégant et bien fonctionner non?
    Rodrigue

  6. #6
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Hé bien, tant qu'à faire, je rendrais les constructeurs et opérateurs d'affectation privés (ou en tout cas protégés), et la fonction generate statique et renvoyant une instance de l'objet créé prenant une forme sans doute fort proche de
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
     
    Montype Montype::generate(/*...*/)
    {
        Montype ret(/*...*/);/*créer une instance de l'objet */
        /*tout ce que doit faire generate */
        return ret;
    }
    De cette manière, au lieu de faire un
    on passerait à
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    Montype obj=Montype::generate(/*...*/);
    J'envisagerais donc un peu les choses un peu comme s'il s'agissait d'un singleton, tout en supprimant ce qui est le plus embêtant dans l'histoire: le fait que l'instance est unique
    A méditer: La solution la plus simple est toujours la moins compliquée
    Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
    Compiler Gcc sous windows avec MinGW
    Coder efficacement en C++ : dans les bacs le 17 février 2014
    mon tout nouveau blog

  7. #7
    Membre expérimenté

    Profil pro
    Inscrit en
    Juin 2006
    Messages
    1 294
    Détails du profil
    Informations personnelles :
    Localisation : Royaume-Uni

    Informations forums :
    Inscription : Juin 2006
    Messages : 1 294
    Points : 1 543
    Points
    1 543
    Par défaut
    Là comme ça je pense que je ne mettrais sans doute pas d'héritage mais que je construirais l'une classe avec l'autre. Quant à savoir si c'est MainA qui demanderait à ExpA de lui remplir son vecteur ou si c'est ExpA qui ferait des ajouts sur MainA j'avoue que j'en sais trop rien à l'avance, surtout sans manipuler des cas réels en tant qu'utilisateur.
    Peut-être qu'en mettant MainA template sur ExpA (avec du coup Generate statique) pourrait aussi donner quelque chose d'intéressant.

    En tous cas j'évite au maximum les héritages quand je peux, en général ça permet de mieux séparer les fonctionnalités et c'est finalement plus souple.

    MAT.

Discussions similaires

  1. Réponses: 4
    Dernier message: 22/11/2010, 14h15
  2. Réponses: 8
    Dernier message: 22/03/2010, 16h52
  3. Réponses: 6
    Dernier message: 24/03/2009, 16h17
  4. Appel d'une méthode virtuelles
    Par BIPBIP59 dans le forum C++Builder
    Réponses: 4
    Dernier message: 24/03/2006, 14h00
  5. Réponses: 3
    Dernier message: 06/11/2005, 18h02

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo