8
votes

Pourquoi le constructeur des classes dérivées veut-il initialiser la classe de base virtuelle en C ++?

Ma compréhension, par exemple en lisant ceci, est que le constructeur d'une classe dérivée n'appelle pas le constructeur de sa classe de base virtuelle.

Voici un exemple simple que j'ai fait:

$ g++ --version
g++ (GCC) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ g++ test.cpp
test.cpp: In constructor ‘B::B()’:
test.cpp:8:13: error: no matching function for call to ‘A::A()’
    8 |         B() {}
      |             ^
test.cpp:3:9: note: candidate: ‘A::A(int)’
    3 |         A(int foo) {}
      |         ^
test.cpp:3:9: note:   candidate expects 1 argument, 0 provided
test.cpp:1:7: note: candidate: ‘constexpr A::A(const A&)’
    1 | class A {
      |       ^
test.cpp:1:7: note:   candidate expects 1 argument, 0 provided
test.cpp:1:7: note: candidate: ‘constexpr A::A(A&&)’
test.cpp:1:7: note:   candidate expects 1 argument, 0 provided
test.cpp: In constructor ‘C::C()’:
test.cpp:13:13: error: no matching function for call to ‘A::A()’
   13 |         C() {}
      |             ^
test.cpp:3:9: note: candidate: ‘A::A(int)’
    3 |         A(int foo) {}
      |         ^
test.cpp:3:9: note:   candidate expects 1 argument, 0 provided
test.cpp:1:7: note: candidate: ‘constexpr A::A(const A&)’
    1 | class A {
      |       ^
test.cpp:1:7: note:   candidate expects 1 argument, 0 provided
test.cpp:1:7: note: candidate: ‘constexpr A::A(A&&)’
test.cpp:1:7: note:   candidate expects 1 argument, 0 provided

Pour une raison quelconque, les constructeurs B::B() et C::C() essaient d'initialiser A (qui, encore une fois dans ma compréhension, aurait déjà dû être initialisé par D à ce stade):

class A {
    protected:
        A(int foo) {}
};

class B: public virtual A {
    protected:
        B() {}
};

class C: public virtual A {
    protected:
        C() {}
};

class D: public B, public C {
    public:
        D(int foo, int bar) :A(foo) {}
};


int main()
{
    return 0;
}

Je suis certain qu'il y a quelque chose de très basique que j'ai mal compris ou que je fais mal, mais je ne sais pas quoi.


2 commentaires

Lors de la construction d'une classe dérivée, toutes les bases sont construites. Les bases virtuelles sont spéciales, en ce qu'elles sont construites en premier, sur la base de la liste d'initialisation de la classe la plus dérivée du constructeur. Si le constructeur de classe dérivée n'initialise pas explicitement sa ou ses bases virtuelles, la base virtuelle est, par défaut, initialisée à l'aide de son constructeur par défaut. C'est le cas parce que les bases virtuelles sont initialisées AVANT les bases non virtuelles - ce qui signifie qu'elles sont initialisées avant que les constructeurs d'autres bases ne soient appelés (ou que leurs listes d'initialisation aient effet).


Pensez "compilation séparée"


3 Réponses :


4
votes

Le constructeur de la base virtuelle est construit. Il est construit conditionnellement. Autrement dit, le constructeur de la classe la plus dérivée appelle le constructeur de la base virtuelle. Si - c'est la condition - la classe dérivée avec la base virtuelle n'est pas la classe concrète de l'objet construit, alors elle ne construira pas la base virtuelle car elle a déjà été construite par la classe concrète. Mais sinon, il construira la base virtuelle.

Ainsi, vous devez correctement initialiser la classe de base virtuelle dans les constructeurs de toutes les classes dérivées. Vous devez simplement savoir qu'une initialisation spécifique ne se produit pas nécessairement dans le cas où la classe concrète n'est pas celle que vous écrivez. Le compilateur ne sait pas et ne peut pas savoir si vous créerez un jour des instances directes de ces classes intermédiaires, il ne peut donc pas simplement ignorer leurs constructeurs cassés.

Si vous avez rendu ces classes intermédiaires abstraites, alors le compilateur saurait qu'elles ne sont jamais du type le plus concret et ainsi leur constructeur ne serait pas obligé d'initialiser la base virtuelle.


0 commentaires

3
votes

Pour une raison quelconque, les constructeurs B :: B () et C :: C () essaient d'initialiser A (qui, encore une fois dans ma compréhension, aurait déjà dû être initialisé par D à ce stade):

Mais que doit faire le compilateur si quelqu'un construit C solo? L'objet final D appellera le constructeur de A mais vous définissez le constructeur de C ce qui implique qu'il peut être construit mais que le constructeur est défectueux car il ne peut pas construire A


2 commentaires

Je pensais que la protection du constructeur était suffisante pour faire comprendre au compilateur que je voulais que la classe soit abstraite. Dans ce cas, personne d'autre que D ne peut construire C


@scozy La pensée de la classe abstraite est bonne, cependant ... De la classe abstraite @ cppreference.com : "Une classe abstraite est une classe qui définit ou hérite d'au moins une fonction pour laquelle le surchargeur final est purement virtuel ." Période. Fin de définition. Le compilateur n'est pas autorisé à étendre cette définition aux classes qu'il pourrait penser que vous voulez être abstraites.



0
votes

En mettant de côté les hiérarchies de classes plus complexes, pour tout type dérivé, il existe exactement une copie de sa base virtuelle. La règle est que le constructeur du type le plus dérivé construit cette base. Le compilateur doit générer du code pour gérer la comptabilité pour cela:

struct D1 : D { };
D1 d1; // `D1` constructor initializes `B` subobject

Jusqu'à présent, c'est assez facile à imaginer, puisque l'initialisation se fait de la même manière qu'elle le serait si B n'était pas une base virtuelle.

Mais alors vous faites ceci:

D d; // which constructor initializes `B` subobject?

Si la base n'était pas virtuelle, le constructeur I1 initialiserait son sujet B et le constructeur I2 initialiserait son sous-objet B Mais parce que c'est virtuel, il n'y a qu'un seul objet B Alors, quel constructeur devrait l'initialiser? Le langage dit que le constructeur D est responsable de cela.

Et la prochaine complication:

struct B { };
struct I1 : virtual B { };
struct I2 : virtual B { };
struct D : I1, I2 { };

B b;   // `B` constructor initializes `B`
I1 i1; // `I1` constructor initializes `B` subobject
I2 i2; // `I2` constructor initializes `B` subobject

Ainsi, en cours de route, nous avons créé cinq objets différents, chacun avec une base virtuelle de type B , et chacun avec le sous-objet B construit à partir d'un constructeur différent.

Attribuer la responsabilité au type le plus dérivé rend l'initialisation facile à comprendre et à visualiser. Il aurait pu y avoir d'autres règles, mais celle-ci est vraiment la plus simple.


0 commentaires