Perché i costruttori non sono ereditati?


33

Sono confuso su quali potrebbero essere i problemi se un costruttore fosse ereditato da una classe base. Cpp Primer Plus dice:

I costruttori sono diversi dagli altri metodi di classe in quanto creano nuovi oggetti, mentre altri metodi sono invocati da oggetti esistenti . Questo è uno dei motivi per cui i costruttori non sono ereditati . Eredità significa che un oggetto derivato può usare un metodo di classe base, ma, nel caso dei costruttori, l'oggetto non esiste fino a quando il costruttore non ha fatto il suo lavoro.

Comprendo che il costruttore viene chiamato prima che la costruzione dell'oggetto sia completata.

Come può causare problemi se una classe figlio eredita ( ereditando intendo che la classe figlio è in grado di sovrascrivere il metodo della classe genitore ecc. Non solo accedendo al metodo della classe genitore ) il costruttore principale?

Capisco che non è necessario chiamare esplicitamente un costruttore all'interno di un codice [non di cui sono ancora a conoscenza.] Se non durante la creazione di oggetti. Anche allora puoi farlo usando un meccanismo per invocare il costitutivo genitore [In cpp, usando ::o usando member initialiser list, In java usando super]. In Java esiste un'applicazione per chiamarlo nella prima riga, capisco che è per garantire che l'oggetto genitore sia creato per primo e poi la costruzione dell'oggetto figlio proceda.

Può ignorarlo . Ma non riesco a trovare situazioni in cui ciò possa rappresentare un problema. Se il figlio eredita il costruttore principale, cosa può andare storto?

Quindi è solo per evitare di ereditare funzioni non necessarie. O c'è di più?


Non sono davvero al passo con C ++ 11, ma penso che ci sia una qualche forma di eredità del costruttore in esso.
yannis,

3
Tieni presente che qualsiasi cosa tu faccia nei costruttori, devi prenderti cura dei distruttori.
Manoj R,

2
Penso che devi definire cosa si intende per "ereditare un costruttore". Tutte le risposte sembrano avere variazioni su ciò che pensano che "ereditare un costruttore" significhi. Non credo che nessuno di loro corrisponda alla mia interpretazione iniziale. La descrizione "nella casella grigia" dall'alto "L'ereditarietà indica che un oggetto derivato può utilizzare un metodo di classe base" implica che i costruttori sono ereditati. Un oggetto derivato sicuramente può e usa SEMPRE metodi di costruzione di classe base.
Dunk,

1
Grazie per il chiarimento di ereditare un costruttore, ora puoi ottenere alcune risposte.
Dunk

Risposte:


41

Non è possibile ereditare correttamente i costruttori in C ++, poiché il costruttore di una classe derivata deve eseguire azioni aggiuntive che un costruttore di classe base non deve fare e di cui non è a conoscenza. Queste azioni aggiuntive sono l'inizializzazione dei membri dei dati della classe derivata (e in un'implementazione tipica, impostando anche il vpointer per fare riferimento alla classe derivata vtable).

Quando una classe viene costruita, ci sono un certo numero di cose che devono sempre accadere: i costruttori della classe base (se presenti) e i membri diretti devono essere chiamati e se ci sono funzioni virtuali, il vpointer deve essere impostato correttamente . Se non fornisci un costruttore per la tua classe, il compilatore ne creerà uno che esegue le azioni richieste e nient'altro. Se lo fai fornire un costruttore, ma perdere su alcune delle azioni necessarie (ad esempio, l'inizializzazione di alcuni membri), allora il compilatore aggiungerà automaticamente le azioni mancanti al costruttore. In questo modo, il compilatore assicura che ogni classe abbia almeno un costruttore e che ogni costruttore inizializzi completamente gli oggetti che crea.

In C ++ 11, è stata introdotta una forma di "ereditarietà del costruttore" in cui è possibile indicare al compilatore di generare un insieme di costruttori che prendono gli stessi argomenti dei costruttori dalla classe base e che inoltrano tali argomenti al classe base.
Sebbene sia ufficialmente chiamato ereditarietà, non lo è davvero perché esiste ancora una funzione specifica di classe derivata. Ora viene appena generato dal compilatore anziché scritto esplicitamente da te.

Questa funzione funziona in questo modo:

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;
};

Derivedora ha due costruttori (senza contare i costruttori copia / sposta). Uno che accetta un int e una stringa e uno che richiede solo un int.


Quindi questo presuppone, la classe figlio non avrà il suo costruttore? e il costruttore ereditato ovviamente non è a conoscenza dei membri del bambino e delle inizializzazioni da fare
Suvarna Pattayil,

Il C ++ è definito in modo tale che è impossibile per una classe non avere i propri costruttori. Questo è il motivo per cui non considero l'ereditarietà del costruttore come eredità. È solo un modo per evitare di scrivere una noiosa caldaia.
Bart van Ingen Schenau,

2
Penso che la confusione derivi dal fatto che anche un costruttore vuoto lavora dietro le quinte. Quindi il costruttore ha davvero due parti: le opere interne e la parte che scrivi. Dal momento che non sono costruttori ben separati non vengono ereditati.
Sarien

1
Si prega di considerare di indicare da dove m()proviene e come cambierebbe se il suo tipo fosse per esempio int.
Deduplicatore

È possibile fare using Base::Baseimplicitamente? Avrebbe grandi conseguenze se dimenticassi quella riga su una classe derivata e dovessi ereditare il costruttore in tutte le classi derivate
Post Self

7

Non è chiaro cosa intendi per "ereditare il costruttore principale". Usi la parola override , il che suggerisce che potresti pensare a costruttori che si comportano come funzioni virtuali polimorfiche . Non uso deliberatamente il termine "costruttori virtuali" perché è un nome comune per un modello di codice in cui è effettivamente necessaria un'istanza già esistente di un oggetto per crearne un altro.

C'è poca utilità per i costruttori polimorfici al di fuori del modello del "costruttore virtuale", ed è difficile elaborare uno scenario concreto in cui si possa usare un vero costruttore polimorfico. Un esempio molto inventato che non è in alcun modo valido anche in remoto 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.
}

In questo caso il costruttore chiamato dipende dal tipo concreto della variabile che viene costruita o assegnata. È complesso da rilevare durante la generazione di analisi / codice e non ha utilità: conosci il tipo concreto che stai costruendo e hai scritto un costruttore specifico per la classe derivata. Il seguente codice C ++ valido fa esattamente lo stesso, è leggermente più corto ed è più esplicito in ciò che fa:

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);
}


Una seconda interpretazione o forse una domanda aggiuntiva è: cosa succede se i costruttori di classi Base fossero automaticamente presenti in tutte le classi derivate se non esplicitamente nascoste.

Se il figlio eredita il costruttore principale, cosa può andare storto?

Dovresti scrivere un codice aggiuntivo per nascondere un costruttore principale che non è corretto da utilizzare nella costruzione della classe derivata. Questo può accadere quando la classe derivata specializza la classe base in modo che determinati parametri diventino irrilevanti.

L'esempio tipico sono rettangoli e quadrati (si noti che i quadrati e i rettangoli non sono generalmente sostituibili a Liskov, quindi non è un ottimo design, ma evidenzia il problema).

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

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

Se Square ereditasse il costruttore a due valori di Rectangle, potresti costruire quadrati con un'altezza e una larghezza diverse ... È logicamente sbagliato, quindi vorrai nascondere quel costruttore.


3

Perché i costruttori non sono ereditati: la risposta è sorprendentemente semplice: il costruttore della classe base "costruisce" la classe base e il costruttore della classe ereditata "costruisce" la classe ereditata. Se la classe ereditata ereditasse il costruttore, il costruttore proverebbe a costruire un oggetto di tipo classe base e non si sarebbe in grado di "costruire" un oggetto di tipo classe ereditata.

Che tipo di sconfitte ha lo scopo di ereditare una classe.


3

Il problema più ovvio nel consentire alla classe derivata di sovrascrivere il costruttore della classe base è che lo sviluppatore della classe derivata è ora responsabile di sapere come costruire le sue classi base. Cosa succede quando la classe derivata non costruisce correttamente la classe base?

Inoltre, il principio di sostituzione di Liskov non si applicherebbe più in quanto non è più possibile fare affidamento sulla propria raccolta di oggetti della classe base per essere compatibili tra loro, poiché non esiste alcuna garanzia che la classe base sia stata costruita correttamente o compatibile con gli altri tipi derivati.

È ulteriormente complicato quando viene aggiunto più di 1 livello di eredità. Ora la tua classe derivata deve sapere come costruire tutte le classi base lungo la catena.

Quindi cosa succede se aggiungi una nuova classe base all'inizio della gerarchia ereditaria? Dovresti aggiornare tutti i costruttori di classi derivate.


2

I costruttori sono fondamentalmente diversi dagli altri metodi:

  1. Sono generati se non li scrivi.
  2. Tutti i costruttori della classe base vengono chiamati implicitamente anche se non lo si fa manualmente
  3. Non li chiami esplicitamente ma creando oggetti.

Quindi perché non sono ereditati? Risposta semplice: poiché esiste sempre una sostituzione, generata o scritta manualmente.

Perché ogni classe ha bisogno di un costruttore? Questa è una domanda complicata e credo che la risposta dipenda dal compilatore. Esiste una cosa come un costruttore "banale" per il quale il compilatore non impone che verrà chiamato, a quanto pare. Penso che sia la cosa più vicina a ciò che intendi per eredità, ma per le tre ragioni sopra menzionate penso che confrontare i costruttori con i metodi normali non sia davvero utile. :)


1

Ogni classe ha bisogno di un costruttore, anche quelli predefiniti.
C ++ creerà costruttori predefiniti per te, tranne se crei un costruttore specializzato.
Nel caso in cui la tua classe di base utilizzi un costruttore specializzato, dovrai scrivere il costruttore specializzato sulla classe derivata anche se entrambi sono uguali e ricollegarli.
C ++ 11 ti consente di evitare la duplicazione del codice sui costruttori usando :

Class A : public B {
using B:B;
....
  1. È composto un insieme di costruttori ereditari

    • Tutti i costruttori non template della classe base (dopo aver omesso i parametri dei puntini di sospensione, se presenti) (dal C ++ 14)
    • Per ogni costruttore con argomenti predefiniti o il parametro con i puntini di sospensione, tutte le firme dei costruttori che si formano facendo cadere i puntini di sospensione e omettendo gli argomenti predefiniti dalla fine degli elenchi uno alla volta
    • Tutti i modelli di costruzione della classe base (dopo aver omesso i parametri dei puntini di sospensione, se presenti) (dal C ++ 14)
    • Per ogni modello di costruttore con argomenti predefiniti o i puntini di sospensione, tutte le firme dei costruttori che si formano facendo cadere i puntini di sospensione e omettendo gli argomenti predefiniti dalle estremità degli elenchi uno alla volta
  2. Tutti i costruttori ereditati che non sono il costruttore predefinito o il costruttore copia / sposta e le cui firme non corrispondono ai costruttori definiti dall'utente nella classe derivata, vengono implicitamente dichiarati nella classe derivata. I parametri predefiniti non sono ereditati


0

Puoi usare:

MyClass() : Base()

Stai chiedendo il perché devi farlo?

La sottoclasse potrebbe avere proprietà aggiuntive che potrebbero dover essere inizializzate nel costruttore, oppure potrebbe inizializzare le variabili della classe base in modo diverso.

In quale altro modo creare l'oggetto di sottotipo?


Grazie. In realtà, sto cercando di capire perché un costruttore non è ereditato nella classe figlio, come altri metodi della classe genitore.
Suvarna Pattayil,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.