Héritage virtuel et constructeur.
Voici une petite histoire d'exploration du language, avec quelques questions et suggestions.
Ces informations pourraient je pense être utiles à mettre dans la FAQ :
Ma question initiale était :
Est-ce qu'un constructeur de class héritée virtuellement est appelée autant de fois qu'il y a d'héritage virtuel de cette classe où bien seulement autant de fois qu'il y a d'occurences de cette classe dans les données de l'occurence fille?
A partir de là, tout s'enchaine...
Une recherche dans la FAQ m'indique qu'il n'y a aucune information concernant l'héritage virtuel qui je le rappelle s'effectue sous cette forme aproximative :
Code:
1 2 3 4 5 6 7 8 9
|
class A {}; // classe mère
class B : virtual public A { } ; // classe fille avec héritage virtuel
class C : virtual public A { } ; // classe fille avec héritage virtuel
class X : public B , public C { }; // class fille dont les données de A seront "partagées" entre B et C , ce qui fait qu'il n'y a "pas" de problème de losange. |
(plus de détails sur la MSDN ou autre doc C++ j'imagine).
Je ne connais cette feature du language moi-même que depuis quelques mois, mais j'en ai une utilisation a priori adequate sur le système sur lequel je travail actuellement. Je trouve que c'est une feature super interessante dans certains cas. Par contre, j'ai vu très très très peu de doc là dessus, dont dans la FAQ.
Donc :
1) Une entrée concernant cette feature dans la FAQ serait je pense interessante, principalement parceque http://cpp.developpez.com/faq/cpp/in...NITION_virtual sous entends que le mot clé "virtual" ne sert qu'a définir des fonctions virtuelles, or c'est faux.
2) Est-ce qu'il y a une raison, une conséquence de la feature, qui la rends impopulaire? Ou bien est-ce simplement le manque d'interet habituel pour cette feature qui fait qu'on en parle jamais? Par exemple, apparamment ça semble résoudre le problème classique du losange. Est-ce correct? Si oui, pourquoi n'est-ce pas une solution plus connue? Y-t-il réellement une raison?
Arrivé a ce questionnement, je me dit qu'une petite preuve serait facile à mettre en place. Je fais un programme de test avec VS2008Pro (VC9).
Voici le programme exact initial :
Code:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
|
#include <iostream>
#define nullptr NULL
class TestA
{
public:
TestA()
{
std::cout << "-> TEST A" << std::endl;
}
virtual ~TestA(){}
virtual void doIt() = nullptr;
};
class TestB : virtual public TestA
{
public:
TestB()
: TestA()
{
std::cout << "-> TEST B" << std::endl;
}
virtual ~TestB(){}
};
class TestC : virtual public TestA
{
public:
TestC()
: TestA()
{
std::cout << "-> TEST C" << std::endl;
}
virtual ~TestC(){}
};
class FinalTest
: virtual public TestB
, virtual public TestC
{
public:
FinalTest()
: TestB()
, TestC()
{
std::cout << "-> FINAL TEST!!" << std::endl;
}
~FinalTest()
{
}
void doIt()
{
std::cout<< "JUST DO IT!!!" << std::endl;
}
};
int main()
{
FinalTest finalTest;
finalTest.doIt();
std::system( "pause" );
return 0;
} |
Avec ce compilo, j'obtiens le résultat suivant :
Code:
1 2 3 4 5 6 7
|
-> TEST A
-> TEST B
-> TEST C
-> FINAL TEST!!
JUST DO IT!!!
Appuyez sur une touche pour continuer... |
3) a) Est-ce bien le résultat que je suis censé obtenir d'après le standard? J'ai apris a me méfier un peu des entourloupes planquées que peuvent faire les compilos avec les parties du languages qui ne sont pas beaucoup utilisées (ou dont j'entends peu parler). Donc, est-ce correct d'un point de vue du standard ou est-ce qu'il y a du VS spécific là dedans?
Ma première interprétation du résultat était que la partie TestA de l'occurence était initialisé donc une seule et unique fois via l'un des autres constructeurs (soit TestB, soit TestC).
C'est alors que je me suis demandé quel pouvait être le constucteur précisément qui était à l'origine de l'appel du constructeur de TestA, car dans le cas où TestA prends un paramettre au constructeur il faudrait que l'auteur du système sache exactement quelle classe fille va fournir le paramettre.
Je modifie donc mon test pour que TestA utilise un paramettre qu'il affiche, indiquant l'origine de l'appel :
Code:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| #include <iostream>
#include <string>
#define nullptr NULL
class TestA
{
public:
TestA( const std::string& msg )
{
std::cout << "-> TEST A [ " << msg << " ]" << std::endl;
}
virtual ~TestA(){}
virtual void doIt() = nullptr;
};
class TestB : virtual public TestA
{
public:
TestB()
: TestA("FROM B")
{
std::cout << "-> TEST B" << std::endl;
}
virtual ~TestB(){}
};
class TestC : virtual public TestA
{
public:
TestC()
: TestA("FROM C")
{
std::cout << "-> TEST C" << std::endl;
}
virtual ~TestC(){}
};
class FinalTest
: public TestB
, public TestC
{
public:
FinalTest()
: TestB()
, TestC()
{
std::cout << "-> FINAL TEST!!" << std::endl;
}
~FinalTest()
{
}
void doIt()
{
std::cout<< "JUST DO IT!!!" << std::endl;
}
};
int main()
{
FinalTest finalTest;
finalTest.doIt();
std::system( "pause" );
return 0;
} |
Quelle surprise quand le compilateur me sort cette erreur :
Code:
1 2
|
main.cpp(53) : error C2512: 'TestA::TestA' : no appropriate default constructor available |
C'est seulement à ce moment là que je comprends que finalement c'est le constructeur de la classe fille TestFinal qui va être a l'origine de l'appel au constructeur de TestA, forcant l'auteur de TestFinal à définir lui-même quel paramettre mettre dans le constructeur de TestA.
Ca semble logique et plutot pratique (évite d'avoir a se prendre la tête avec l'ordre dans l'héritage).
Je corrige donc mon test :
Code:
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| #include <iostream>
#include <string>
#define nullptr NULL
class TestA
{
public:
TestA( const std::string& msg )
{
std::cout << "-> TEST A [ " << msg << " ]" << std::endl;
}
virtual ~TestA(){}
virtual void doIt() = nullptr;
};
class TestB : virtual public TestA
{
public:
TestB()
: TestA("FROM B")
{
std::cout << "-> TEST B" << std::endl;
}
virtual ~TestB(){}
};
class TestC : virtual public TestA
{
public:
TestC()
: TestA("FROM C")
{
std::cout << "-> TEST C" << std::endl;
}
virtual ~TestC(){}
};
class FinalTest
: public TestB
, public TestC
{
public:
FinalTest()
: TestA( "FROM FINAL" )
, TestB()
, TestC()
{
std::cout << "-> FINAL TEST!!" << std::endl;
}
~FinalTest()
{
}
void doIt()
{
std::cout<< "JUST DO IT!!!" << std::endl;
}
};
int main()
{
FinalTest finalTest;
finalTest.doIt();
std::system( "pause" );
return 0;
} |
Ca compile et donne donc comme résultat :
Code:
1 2 3 4 5 6 7
|
-> TEST A [ FROM FINAL ]
-> TEST B
-> TEST C
-> FINAL TEST!!
JUST DO IT!!!
Appuyez sur une touche pour continuer... |
Ce qui semble logique a priori.
3) b) Encore une fois, est-ce toujours standard?
J'ai donc noté que les appels explicites au constructeur de TestA dans TestB et TestC sont ignorés au profit de l'appel de TestFinal... c'est bon à savoir.
Fort de ce savoir, je me dit que normalement, comme TestB et TestC sont virtuels (la méthode TestA::doIt est virtuelle pure et n'est pas redéfinie), théoriquement les appels explicites au constructeur de TestA sont obsoletes puisqu'ils seront TOUJOURS ignorés.
4) Est-ce correct ou est-ce que j'ai oublié quelque chose sur ce point?
Pourtant, je fais une simple modification : je commente les appels a TestA::TestA() dans les constructeurs de TestB et TestC.
Je tente de compiler mais sans succès, à ma grande surprise :
Code:
1 2
| main.cpp(23) : error C2512: 'TestA::TestA' : no appropriate default constructor available
main.cpp(36) : error C2512: 'TestA::TestA' : no appropriate default constructor available |
DONC même si apparamment ces appels au constructeur de TestA ne sont jamais appelés, ils sont tout de même requis.
5) Est-ce toujours Standard ou est-ce du à la façon dont à le compiler de parser le code?
Voilà pour ma petite péripétie et mes questions. A vous messieurs les spécialisttes :aie: