web-dev-qa-db-fra.com

Pourquoi les constructeurs ne sont-ils pas hérités?

Je suis confus quant à ce que pourraient être les problèmes si un constructeur était hérité d'une classe de base. Cpp Primer Plus dit:

Les constructeurs sont différents des autres méthodes de classe en ce qu'ils créent de nouveaux objets, tandis que d'autres méthodes sont invoquées par des objets existants. C'est une raison pour laquelle les constructeurs ne sont pas hérités. L'héritage signifie qu'un objet dérivé peut utiliser une méthode de classe de base, mais, dans le cas des constructeurs, l'objet n'existe pas tant que le constructeur n'a pas terminé son travail.

Je comprends que le constructeur est appelé avant la fin de la construction de l'objet.

Comment cela peut-il entraîner des problèmes si une classe enfant hérite ( En héritant, je veux dire que la classe enfant est capable de remplacer la méthode de classe parent, etc. Pas seulement d'accéder à la méthode de classe parent) le constructeur parent?

Je comprends qu'il n'est pas nécessaire d'appeler explicitement un constructeur à partir d'un code [pas à ma connaissance pour l'instant.] Sauf lors de la création d'objets. Même dans ce cas, vous pouvez le faire en utilisant un mécanisme pour appeler le constructeur parent [En cpp, en utilisant :: ou en utilisant member initialiser list, In Java using super]. In Java il y a une application pour l'appeler en 1ère ligne, je comprends que c'est pour vous assurer que l'objet parent est d'abord créé, puis la construction de l'objet enfant se poursuit.

Il peut outrepasser le. Mais, je ne peux pas trouver de situations où cela peut poser un problème. Si l'enfant hérite du constructeur parent, qu'est-ce qui peut mal tourner?

Il en va de même pour ne pas hériter des fonctions inutiles. Ou y a-t-il plus?

34
Suvarna Pattayil

Il ne peut pas y avoir d'héritage approprié de constructeurs en C++, car le constructeur d'une classe dérivée doit effectuer des actions supplémentaires qu'un constructeur de classe de base n'a pas à faire et ne connaît pas. Ces actions supplémentaires sont l'initialisation des membres de données de la classe dérivée (et dans une implémentation typique, définissant également le vpointer pour qu'il fasse référence aux classes dérivées vtable).

Lorsqu'une classe est construite, il y a un certain nombre de choses qui doivent toujours se produire: les constructeurs de la classe de base (le cas échéant) et les membres directs doivent être appelés et s'il existe des fonctions virtuelles, le vpointer doit être défini correctement . Si vous ne fournissez pas de constructeur pour votre classe, alors le compilateur va en créer un qui exécute les actions requises et rien d'autre. Si vous faites fournissez un constructeur, mais manquez certaines des actions requises (par exemple, l'initialisation de certains membres), alors le compilateur ajoutera automatiquement les actions manquantes à votre constructeur. De cette façon, le compilateur garantit que chaque classe possède au moins un constructeur et que chaque constructeur initialise entièrement les objets qu'il crée.

En C++ 11, une forme d'héritage de constructeur a été introduite où vous pouvez demander au compilateur de générer pour vous un ensemble de constructeurs qui prennent les mêmes arguments que les constructeurs de la classe de base et qui transmettent ces arguments au classe de base.
Bien qu'il soit officiellement appelé héritage, ce n'est pas vraiment le cas car il existe toujours une fonction spécifique de classe dérivée. Maintenant, il est simplement généré par le compilateur au lieu d'être explicitement écrit par vous.

Cette fonctionnalité fonctionne comme ceci:

struct Base {
    Base(int a) : i(a) {}
    int i;
};

struct Derived : Base {
    Derived(int a, std::string s) : Base(a), m(s) {}

    using Base::Base; // Inherit Base's constructors.
    // Equivalent to:
    //Derived(int a) : Base(a), m() {}

    std::string m;
};

Derived a maintenant deux constructeurs (sans compter les constructeurs copier/déplacer). Un qui prend un int et une chaîne et un qui ne prend qu'un int.

41

Ce que vous entendez par "hériter du constructeur parent" n'est pas clair. Vous utilisez le mot qui remplace , ce qui suggère que vous pensez peut-être à constructeurs qui se comportent comme des fonctions virtuelles polymorphes . Je n'utilise délibérément pas le terme "constructeurs virtuels" car c'est un nom commun pour un modèle de code où vous avez réellement besoin d'une instance déjà existante d'un objet pour en créer un autre.

Il y a peu d'utilité pour les constructeurs polymorphes en dehors du modèle "constructeur virtuel", et il est difficile de trouver un scénario concret où un constructeur polymorphe réel pourrait être utilisé. Un exemple très artificiel que n'est en aucun cas même valide à distance C++ :

struct Base {
  virtual Base(unsigned p1, unsigned p2) {...}
};

struct Derived: public Base {
  Derived(unsigned p1, unsigned p2) : Base(p1, p2) override {...}
};

int main(void) {
  unsigned p1 = 0;
  unsigned p2 = 42;
  Derived *p_d1 = new Base(p1, p2); // This might call Derived(unsigned, unsigned).
  Derived *p_d2 = nullptr;
  p_d2 = new Base(p1, p2); // This might call Derived(unsigned, unsigned) too.
}

Dans ce cas, le constructeur appelé dépend du type concret de la variable en cours de construction ou affectée. Il est complexe à détecter lors de l'analyse/génération de code et n'a aucune utilité: vous connaissez le type concret que vous construisez et vous avez écrit un constructeur spécifique pour la classe dérivée. Le code C++ valide suivant fait exactement la même chose, est légèrement plus court et plus explicite dans ce qu'il fait:

struct Base {
  Base(unsigned p1, unsigned p2) {...}
};

struct Derived: public Base {
  Derived(unsigned p1, unsigned p2) : Base(p1, p2) {...}
};

int main(void) {
  unsigned p1 = 0;
  unsigned p2 = 42;
  Derived *p_d1 = new Derived(p1, p2); 
  Derived *p_d2 = nullptr;
  p_d2 = new Derived(p1, p2);
}


Une deuxième interprétation ou peut-être une question supplémentaire est - et si les constructeurs de classe de base étaient automatiquement présents dans toutes les classes dérivées, sauf s'ils sont explicitement cachés.

Si l'enfant hérite du constructeur parent, qu'est-ce qui peut mal tourner?

Vous devez écrire du code supplémentaire pour masquer un constructeur parent incorrect à utiliser dans la construction de la classe dérivée. Cela peut se produire lorsque la classe dérivée spécialise la classe de base de manière à ce que certains paramètres deviennent inutiles.

L'exemple typique est les rectangles et les carrés (notez que les carrés et les rectangles ne sont généralement pas substituables à Liskov, donc ce n'est pas une très bonne conception, mais cela met en évidence le problème).

struct Rectangle {
  Rectangle(unsigned width, unsigned height) {...}
};

struct Square : public Rectangle {
  explicit Square(unsigned side) : Rectangle(side, side) {...}
};

Si Square a hérité du constructeur à deux valeurs de Rectangle, vous pourriez construire des carrés avec une hauteur et une largeur différentes ... C'est logiquement faux, donc vous voudriez cacher ce constructeur.

7
Joris Timmermans

Pourquoi les constructeurs ne sont-ils pas hérités: la réponse est étonnamment simple: le constructeur de la classe de base "construit" la classe de base et le constructeur de la classe héritée "construit" la classe héritée. Si la classe héritée héritait du constructeur, le constructeur essaierait de construire un objet de type classe de base et vous ne seriez pas en mesure de "construire" un objet de type classe héritée.

Quel genre de défait le but d'hériter d'une classe.

3
Pieter B

Le problème le plus évident avec le fait d'autoriser la classe dérivée à remplacer le constructeur de la classe de base est que le développeur de la classe dérivée est maintenant responsable de savoir comment construire sa ou ses classes de base. Que se passe-t-il lorsque la classe dérivée ne construit pas correctement la classe de base?

En outre, le principe de substitution de Liskov ne s'appliquerait plus, car vous ne pouvez plus compter sur votre collection d'objets de classe de base pour être compatible les uns avec les autres, car il n'y a aucune garantie que la classe de base a été construite correctement ou de manière compatible avec les autres types dérivés.

C'est encore plus compliqué lorsque plus d'un niveau d'héritage est ajouté. Votre classe dérivée doit maintenant savoir comment construire toutes les classes de base en amont.

Que se passe-t-il alors si vous ajoutez une nouvelle classe de base en haut de la hiérarchie d'héritage? Vous devez mettre à jour tous les constructeurs de classes dérivés.

3
Dunk

Les constructeurs sont fondamentalement différents des autres méthodes:

  1. Ils sont générés si vous ne les écrivez pas.
  2. Tous les constructeurs de classe de base sont appelés implicitement même si vous ne le faites pas manuellement
  3. Vous ne les appelez pas explicitement mais en créant des objets.

Alors pourquoi ne sont-ils pas hérités? Réponse simple: Parce qu'il y a toujours un remplacement, généré ou écrit manuellement.

Pourquoi chaque classe a-t-elle besoin d'un constructeur? C'est une question compliquée et je pense que la réponse dépend du compilateur. Il existe une chose telle qu'un constructeur "trivial" pour lequel le compilateur n'exige pas qu'il soit appelé, semble-t-il. Je pense que c'est la chose la plus proche de ce que vous entendez par héritage, mais pour les trois raisons énoncées ci-dessus, je pense que la comparaison des constructeurs aux méthodes normales n'est pas vraiment utile. :)

2
Sarien

Chaque classe a besoin d'un constructeur, même ceux par défaut.
C++ créera des constructeurs par défaut pour vous, sauf si vous créez un constructeur spécialisé.
Dans le cas où votre classe de base utilise un constructeur spécialisé, vous devrez écrire le constructeur spécialisé sur la classe dérivée même si les deux sont identiques et les enchaîner.
C++ 11 vous permet d'éviter la duplication de code sur les constructeurs en utilisant en utilisant:

Class A : public B {
using B:B;
....
  1. Un ensemble de constructeurs hérités est composé de

    • Tous les constructeurs non modèles de la classe de base (après avoir omis les paramètres Ellipsis, le cas échéant) (depuis C++ 14)
    • Pour chaque constructeur avec des arguments par défaut ou le paramètre Ellipsis, toutes les signatures de constructeur qui sont formées en supprimant les points de suspension et en omettant les arguments par défaut à la fin des listes d'arguments une par une
    • Tous les modèles de constructeur de la classe de base (après avoir omis les paramètres Ellipsis, le cas échéant) (depuis C++ 14)
    • Pour chaque modèle de constructeur avec des arguments par défaut ou les points de suspension, toutes les signatures de constructeur qui sont formées en supprimant les points de suspension et en omettant les arguments par défaut à la fin des listes d'arguments une par une
  2. Tous les constructeurs hérités qui ne sont pas le constructeur par défaut ou le constructeur copier/déplacer et dont les signatures ne correspondent pas aux constructeurs définis par l'utilisateur dans la classe dérivée, sont implicitement déclarés dans la classe dérivée. Les paramètres par défaut ne sont pas hérités

1
MeduZa

Vous pouvez utiliser:

MyClass() : Base()

Demandez-vous pourquoi vous devez le faire?

La sous-classe pourrait avoir des propriétés supplémentaires qui pourraient devoir être initialisées dans le constructeur, ou elle pourrait initialiser les variables de classe de base d'une manière différente.

Sinon, comment pourriez-vous créer l'objet sous-type?

0
Tom Tom