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 :
(plus de détails sur la MSDN ou autre doc C++ j'imagine).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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.
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 :
Avec ce compilo, j'obtiens le résultat suivant :
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
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; }
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?
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 -> TEST A -> TEST B -> TEST C -> FINAL TEST!! JUST DO IT!!! Appuyez sur une touche pour continuer...
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 :
Quelle surprise quand le compilateur me sort cette erreur :
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
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; }
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.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 main.cpp(53) : error C2512: 'TestA::TestA' : no appropriate default constructor available
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 :
Ca compile et donne donc comme résultat :
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
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; }
Ce qui semble logique a priori.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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...
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 :
DONC même si apparamment ces appels au constructeur de TestA ne sont jamais appelés, ils sont tout de même requis.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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
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![]()
Partager