La chiamata di funzioni virtuali all’interno di costruttori

Supponiamo di avere due classi C++:

class A
{
public:
  A() { fn(); }

  virtual void fn() { _n = 1; }
  int getn() { return _n; }

protected:
  int _n;
};

class B : public A
{
public:
  B() : A() {}

  virtual void fn() { _n = 2; }
};

Se scrivo il seguente codice:

int main()
{
  B b;
  int n = b.getn();
}

Ci si potrebbe aspettare che n è impostato su 2.

Si scopre che n è impostato a 1. Perché?

Mi sto chiedendo e rispondendo alla mia domanda perché voglio ottenere la spiegazione di questo po ‘ di C++ esoterica in Overflow dello Stack. Una versione di questo problema ha colpito il nostro team di sviluppo, per due volte, quindi sto cercando di indovinare che questa info possa essere utile a qualcuno. Si prega di scrivere una risposta se è possibile spiegarlo in modo diverso/migliore modo…
Mi chiedo il perché di questo si sono messi votato? Quando in primo luogo ho imparato il C++, questo davvero mi ha confuso. +1

OriginaleL’autore David Coufal | 2009-06-07

13 Replies
  1. 180

    Chiamata di funzioni virtuali da un costruttore o distruttore è pericoloso e deve essere evitato quando è possibile. Tutti C++ implementazioni dovrebbe chiamare la versione della funzione definita a livello di gerarchia nell’attuale costruttore, e nessun altro.

    Il C++ FAQ Lite copre questa sezione del 23,7 piuttosto un buon dettaglio. Suggerisco la lettura di questo (e il resto della FAQ) per i follow-up.

    Estratto:

    […] In un costruttore, il meccanismo di chiamata virtuale è disabilitato perché l’override dalle classi derivate non è ancora accaduto. Gli oggetti sono costruiti dalla base, “di base prima di derivati”.

    […]

    Distruzione è fatto “classe derivata prima classe di base”, in modo che funzioni virtuali si comportano come in costruttori: Solo locale vengono utilizzate le definizioni e le chiamate non sono fatto l’override di funzioni, per evitare di toccare l’ (ora distrutta) classe derivata una parte dell’oggetto.

    MODIFICA Corretto la Maggior parte di Tutti (grazie litb)

    Non più C++ implementazioni, ma tutti C++ implementazioni di chiamare l’attuale versione della classe. Se alcuni non, quindi, quelli hanno un bug :). Io ancora d’accordo con te che è un male per chiamare una funzione virtuale da una classe base, ma la semantica è definita in modo preciso.
    Non è pericoloso, è solo non virtuale. Infatti, se i metodi chiamato dal costruttore sono stati chiamati praticamente, sarebbe pericoloso, perché il metodo di accesso non inizializzata membri.
    Perché la chiamata di funzioni virtuali da distruttore pericoloso? Non è l’oggetto ha ancora completato quando il distruttore viene eseguito, e solo distrutto dopo il distruttore finiture?
    -1 “è pericoloso”, no, è pericoloso in Java, dove downcalls può accadere; il C++ regole di rimuovere il pericolo attraverso una piuttosto costoso meccanismo.
    In che modo è chiamata a una funzione virtuale da un costruttore “pericoloso”? Questa è una sciocchezza totale.

    OriginaleL’autore JaredPar

  2. 77

    Chiamata polimorfi funzione da un costruttore è una ricetta per il disastro, nella maggior parte dei linguaggi OO. Diverse lingue di eseguire in modo diverso quando questa situazione si è verificata.

    Il problema di base è che in tutte le lingue il tipo di Base(s) devono essere costruiti precedente di tipo Derivato. Ora, il problema è che cosa significa per chiamare un metodo polimorfico dal costruttore. Che cosa si aspetta che si comporti come? Ci sono due approcci: chiamare il metodo a livello di Base (stile C++) o chiamare il metodo polimorfico su un unconstructed oggetto in fondo alla gerarchia (Java).

    In C++ la classe Base, costruirà la versione di virtual method table, prima di entrare, di propria costruzione. A questo punto una chiamata al metodo virtuale finirà per chiamare la versione Base del metodo o di produrre un puro metodo virtuale chiamato in caso di non attuazione a livello della gerarchia. Dopo la Base, è stato completamente costruito, il compilatore iniziare a costruire la classe Derivata, e sostituire il metodo di puntatori a punto per le implementazioni nel livello successivo della gerarchia.

    class Base {
    public:
       Base() { f(); }
       virtual void f() { std::cout << "Base" << std::endl; } 
    };
    class Derived : public Base
    {
    public:
       Derived() : Base() {}
       virtual void f() { std::cout << "Derived" << std::endl; }
    };
    int main() {
       Derived d;
    }
    //outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run

    In Java, il compilatore genera il tavolo virtuale equivalente alla prima fase di costruzione, prima di entrare il costruttore di Base o Derivati costruttore. Le implicazioni sono diverse (e per i miei gusti più pericoloso). Se il costruttore della classe base chiama un metodo che è sottoposto a override in una classe derivata, la chiamata sarà effettivamente essere gestite a livello derivato la chiamata di un metodo su un unconstructed oggetto, ottenendo risultati inaspettati. Tutti gli attributi della classe derivata che vengono inizializzati all’interno del costruttore blocco di sicurezza non inizializzata, tra cui il ‘finale’ di attributi. Elementi che hanno un valore predefinito a livello di classe che di valore.

    public class Base {
       public Base() { polymorphic(); }
       public void polymorphic() { 
          System.out.println( "Base" );
       }
    }
    public class Derived extends Base
    {
       final int x;
       public Derived( int value ) {
          x = value;
          polymorphic();
       }
       public void polymorphic() {
          System.out.println( "Derived: " + x ); 
       }
       public static void main( String args[] ) {
          Derived d = new Derived( 5 );
       }
    }
    //outputs: Derived 0
    //         Derived 5
    //... so much for final attributes never changing :P

    Come si può vedere, la chiamata di una polimorfici (virtuale in C++ terminologia) metodi è una comune fonte di errori. In C++, almeno hai la garanzia che non sarà mai chiamata a un metodo di sicurezza unconstructed oggetto…

    Buon lavoro spiegando perché l’alternativa è (anche) sono soggette ad errore.
    “Se il costruttore della classe base chiama un metodo che è sottoposto a override in una classe derivata, la chiamata sarà effettivamente essere gestite a livello derivato la chiamata di un metodo su un unconstructed oggetto…” e Come se la base è già inizializzato. Non c’è nessuna possibilità a meno che non si explicilty chiamata “init” prima di inizializzare gli altri membri.
    Una spiegazione! +1, superiore risposta imho
    Per me il problema è che ci sono così tante restrizioni in classi C++ che con la sua incredibile difficile ottenere nessun buon design. C++ impone che “Se potrebbe essere “pericoloso ” vietarlo” anche se la sua interfaccia intuitiva causare problemi come: “Perché questo intuitiva comportamento non funziona” accadere tutto il tempo.
    Cosa???? C++ non proibire nulla in questo caso. La chiamata è semplicemente trattato come un non-virtuale chiamata al metodo per la classe il cui costruttore è attualmente in esecuzione. Che è una conseguenza logica dell’oggetto timeline costruzione – e non a qualche draconiane decisione di smettere di fare cose stupide. Il fatto che per coincidenza adempie a quest’ultimo scopo è solo un bonus per me.

    OriginaleL’autore David Rodríguez – dribeas

  3. 52

    Il motivo è che il C++ sono costruiti gli oggetti come le cipolle, da dentro e fuori. Super-classi costruite prima le classi derivate. Così, prima di un B può essere fatto, deve essere fatto. Quando Un costruttore viene chiamato, non è una B di sicurezza, in modo che la tabella di funzione virtuale ha ancora la voce per Una copia di fn().

    C++ di solito non uso il termine “super classe” – preferisce “classe di base”.
    Che è la stessa nella maggior parte dei linguaggi OO: non si può pretendere di costruire un oggetto derivato senza la parte di base già costruita.
    lingue effettivamente farlo. Per esempio in Pascal, viene allocata la memoria per tutto il primo oggetto, ma solo la più derivati costruttore viene invocato. Un costruttore deve contenere un esplicito invito ai suoi genitori costruttore (che non deve essere la prima azione è solo di essere da qualche parte), o se non lo è , è come se la prima riga del costruttore fatto quella telefonata.
    Grazie per la chiarezza e per evitare di dettagli che non vanno dritti al risultato

    OriginaleL’autore David Coufal

  4. 22

    Il C++ FAQ Lite Copre questo abbastanza bene:

    Essenzialmente, durante la chiamata, le classi base costruttore, l’oggetto non è ancora del tipo derivato e, quindi, la base del tipo di implementazione della funzione virtuale viene chiamato e non del tipo derivato.

    Chiaro, semplice, la risposta più semplice. È ancora una caratteristica che mi piacerebbe vedere un po ‘ di amore. Odio dover scrivere tutte queste stupide initializeObject() le funzioni che l’utente è costretto a chiamare subito dopo la costruzione, a soli cattiva forma per un caso di uso comune. Capisco la difficoltà però. C est la vie.
    Quello che “amore” proponete? Tenete a mente che non puoi cambiare come stanno le cose attualmente lavoro in un posto, perché sarebbe terribilmente rompere le risme di codice esistente. Così, come farebbe invece? Non solo ciò che la nuova sintassi di introdurre, per consentire (effettivo, non devirtualised) virtuale chiamate in costruttori, ma anche come si dovrebbe, in qualche modo, modificare i modelli di costruzione di oggetti/durata di vita in modo che le chiamate che sarebbe un oggetto di tipo derivato su cui si desidera eseguire. Questo sarà interessante.
    Non credo che eventuali modifiche di sintassi sarebbe necessaria. Forse quando si crea un oggetto, il compilatore aggiungere codice a piedi la vtable e cercare questo caso e patch le cose allora? Non ho mai scritto un compilatore C++ e sono abbastanza sicuro che il mio commento iniziale per dare a questo un “amore” è stato ingenuo e questo non accadrà mai. 🙂 Un virtual funzione initialize() non è molto dolorosa la soluzione comunque, basta ricordarsi di chiamare dopo aver creato l’oggetto.
    Ho appena notato il tuo altro commento qui sotto, spiegando che la vtable non è disponibile nel costruttore, sottolineando nuovamente la difficoltà qui.
    Ho preso una cantonata, quando si scrive la vtable non essere disponibili nel costruttore. è disponibile, ma il costruttore vede solo la vtable per la sua classe, perché ogni derivato costruttore aggiornamenti dell’istanza vptr a punto alla vtable per la corrente di tipo derivato, e nessun altro. Così, la corrente ctor vede una vtable che ha solo la propria sostituisce, quindi, perché è non si può chiamare più derivate implementazioni di qualsiasi funzioni virtuali.

    OriginaleL’autore Aaron Maenpaa

  5. 12

    Una soluzione al tuo problema utilizzando metodi factory per creare l’oggetto.

    • Definire una classe base comune per la tua gerarchia di classe contenente un metodo virtuale afterConstruction():
    Oggetto di classe 
    { 
    pubblico: 
    virtual void afterConstruction() {} 
    //... 
    }; 
    
    • Definire un metodo factory:
    modello< classe C > 
    C* factoryNew() 
    { 
    C* pObject = new C(); 
    pObject->afterConstruction(); 
    
    ritorno pObject; 
    } 
    
    • Usarlo come questo:
    classe MyClass : Oggetto di uso pubblico 
    { 
    pubblico: 
    virtual void afterConstruction() 
    { 
    //fare qualcosa. 
    } 
    //... 
    }; 
    
    MyClass* pMyObject = factoryNew(); 
    
    
    Grazie per la formattazione di miglioramento!

    OriginaleL’autore Tobias

  6. 1

    Non si conosce il crash di errore da Windows explorer?! “Chiamata di funzione virtuale pura …”

    Stesso problema …

    class AbstractClass 
    {
    public:
        AbstractClass( ){
            //if you call pureVitualFunction I will crash...
        }
        virtual void pureVitualFunction() = 0;
    };

    Perché non vi è alcuna implementazione per la funzione pureVitualFunction() e la funzione viene chiamata nel costruttore il programma andrà in crash.

    È difficile vedere come questo è il problema stesso, in quanto non spiega il perché. Chiamate per non funzioni virtuali pure durante ctors sono perfettamente legali, ma semplicemente non passare attraverso il (non ancora costruito) tabella virtuale, in modo che la versione del metodo che viene eseguito è quello definito per il tipo di classe di cui ctor in cui ci troviamo. Così quelli non in crash. Questo si fa perché è virtuale pura e implementate (nota a margine: uno implementare funzioni virtuali pure nella base), quindi non c’è nessuna versione del metodo chiamato per questo tipo di classe, & il compilatore assume che non scrivi male il codice, in modo da boom
    D’oh. Le chiamate non passano attraverso la vtable, ma non è ancora stato aggiornato per il sostituzioni per la maggior parte della classe derivata: solo un essere costruito proprio ora. Ancora, il risultato e la ragione per l’incidente rimane la stessa.

    OriginaleL’autore TimW

  7. 1

    Le tabelle vtable vengono creati dal compilatore.
    Un oggetto di classe è un puntatore alla vtable. Quando inizia la vita, che vtable puntatore punti per la vtable
    della classe di base. Alla fine del codice del costruttore, il compilatore genera codice per reindirizzare la vtable puntatore
    per l’effettivo vtable per la classe. Questo assicura che il codice del costruttore che le chiamate di funzioni virtuali chiamate il
    classe base implementazioni di tali funzioni, non l’override nella classe.

    Il vptr non è cambiato alla fine di un costruttore. Nel corpo del costruttore C::C, funzione virtuale chiamate passare il C overrider, non a qualsiasi versione della classe base.
    puoi spiegare più chiaramente.
    Il tipo dinamico di un oggetto è definito dopo che il costruttore ha chiamato classe base ctors e prima che costruisce i suoi membri. Così il vptr non è cambiato alla fine di un costruttore.
    Io sto dicendo la stessa cosa, che vptr non è cambiato alla fine del costruttore della classe base, verrà modificato al fine di costruttore della classe derivata. Spero che si sta dicendo la stessa cosa. Un compilatore/implementazione dipendente cosa. Quando stai proponendo che vptr dovrebbe cambiare. Una buona ragione per downvoting?
    La tempistica della variazione di vptr non implementazione dipendente. È prescritto da un linguaggio semantica: il vptr cambia quando cambia il comportamento dinamico dell’istanza di classe cambiamenti. Non c’è libertà qui. All’interno del corpo di un costruttore T::T(params), il tipo dinamico è T. Il vptr riflettere che: si punta alla vtable T. siete in disaccordo?

    OriginaleL’autore Yogesh

  8. 1

    Il C++ Standard (ISO/IEC 14882-2014) dire:

    Funzioni membro, comprese le funzioni virtuali (10.3), può essere chiamato
    durante la costruzione o la distruzione (12.6.2). Quando una funzione virtuale
    si chiama, direttamente o indirettamente, da un costruttore o da un
    distruttore, anche durante la costruzione o la distruzione del
    classe non-membri dati statici, e l’oggetto a cui la chiamata
    vale è l’oggetto (chiamiamolo x) in costruzione o la distruzione,
    la funzione chiamata è il finale overrider nel costruttore o
    distruttore della classe e non un override in una classe derivata.
    Se la chiamata di funzione virtuale utilizza un esplicito membro della classe di accesso
    (5.2.5) e l’oggetto espressione si riferisce l’oggetto completo di x
    o un oggetto di classe di base dell’oggetto, ma non x o uno dei suoi
    classe di base oggetto, il comportamento è undefined.

    Così, non invocare virtual funzioni da costruttori o distruttori che tenta di chiamare in oggetto in costruzione o la distruzione, Perché l’ordine di inizio dei lavori di costruzione da base di derivati e l’ordine dei distruttori inizia da derivati a base di classe.

    Così, tentando di chiamare una classe derivata di funzione di una classe di base in costruzione è pericoloso.Allo stesso modo, un oggetto viene distrutto in ordine inverso da costruzione, in modo da tentare di chiamare una funzione in più di una classe derivata da un distruttore può accedere a risorse che sono già state rilasciate.

    OriginaleL’autore M.S Chaudhari

  9. 1

    Altre risposte hanno già spiegato perché virtual chiamate di funzione non funziona come previsto quando viene chiamato da un costruttore. Vorrei invece proporre un’altra possibile risolvere per ottenere polimorfici un comportamento simile da un costruttore del tipo di base.

    Con l’aggiunta di un modello costruttore per il tipo di base tale che il modello argomento è sempre deduce di essere il tipo derivato che è possibile essere a conoscenza del tipo derivato del tipo di calcestruzzo. Da lì, è possibile chiamare static funzioni membro per che tipo derivato.

    Questa soluzione non consente l’-static le funzioni membro per essere chiamato. Mentre è in esecuzione il tipo di base di costruzione, il costruttore del tipo derivato, non ha avuto nemmeno il tempo di passare attraverso di essa, socio di elenco di inizializzazione. La derivata del tipo di parte dell’istanza creata non è iniziato in fase di inizializzazione. E dal momento che nonstatic funzioni membro quasi certamente interagire con i membri di dati che sarebbe insolito per desidera a chiamare il tipo derivato nonstatic funzioni membro dal costruttore del tipo di base.

    Qui è un esempio di implementazione :

    #include <iostream>
    #include <string>
    
    struct Base {
    protected:
        template<class T>
        explicit Base(const T*) : class_name(T::Name())
        {
            std::cout << class_name << " created\n";
        }
    
    public:
        Base() : class_name(Name())
        {
            std::cout << class_name << " created\n";
        }
    
    
        virtual ~Base() {
            std::cout << class_name << " destroyed\n";
        }
    
        static std::string Name() {
            return "Base";
        }
    
    private:
        std::string class_name;
    };
    
    
    struct Derived : public Base
    {   
        Derived() : Base(this) {} //`this` is used to allow Base::Base<T> to deduce T
    
        static std::string Name() {
            return "Derived";
        }
    };
    
    int main(int argc, const char *argv[]) {
    
        Derived{};  //Create and destroy a Derived
        Base{};     //Create and destroy a Base
    
        return 0;
    }

    Questo è un esempio che dovrebbe stampare

    Derived created
    Derived destroyed
    Base created
    Base destroyed

    Quando un Derived è costruito, il Base costruttore del comportamento dipendente dal tipo dinamico dell’oggetto in fase di costruzione.

    OriginaleL’autore François Andrieux

  10. 0

    In primo luogo,viene creato un Oggetto e poi si assegna l ‘indirizzo di puntatori.I costruttori sono chiamati al momento della creazione dell’oggetto e usato per initializ il valore dei membri di dati. Puntatore a un oggetto entra in scenario dopo la creazione dell’oggetto. Ecco perché, in C++ non ci permette di fare costruttori come virtuale .
    .un altro motivo è che, non C’è niente come puntatore al costruttore , che può scegliere virtuale costruttore,perché una delle proprietà della funzione virtuale è che può essere utilizzato da puntatori.

    1. Funzioni virtuali sono utilizzati per assegnare il valore in modo dinamico,come i costruttori sono statici,quindi non è possibile farli virtuale.

    OriginaleL’autore Priya

  11. 0

    Come è stato sottolineato, gli oggetti vengono creati base-down al momento della costruzione. Quando l’oggetto di base viene costruito l’oggetto derivato non esiste ancora, quindi, una funzione virtuale override non può funzionare.

    Tuttavia, questo può essere risolto con polimorfici getter che uso statico polimorfismo invece di funzioni virtuali se il getter ritorno costanti, o altrimenti può essere espressa in funzione membro static, in Questo esempio utilizza CRTP (https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).

    template<typename DerivedClass>
    class Base
    {
    public:
        inline Base() :
        foo(DerivedClass::getFoo())
        {}
    
        inline int fooSq() {
            return foo * foo;
        }
    
        const int foo;
    };
    
    class A : public Base<A>
    {
    public:
        inline static int getFoo() { return 1; }
    };
    
    class B : public Base<B>
    {
    public:
        inline static int getFoo() { return 2; }
    };
    
    class C : public Base<C>
    {
    public:
        inline static int getFoo() { return 3; }
    };
    
    int main()
    {
        A a;
        B b;
        C c;
    
        std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;
    
        return 0;
    }

    Con l’uso del polimorfismo statico, la classe di base sa a quale classe’ getter chiamare come le informazioni vengono fornite al momento della compilazione.

    Penso di evitare di fare questo. Questa non è la singola classe di Base. Si effettivamente creato un sacco di diverse classe di Base.
    Esattamente: Base<T> è solo una classe di supporto, non un comune tipo di interfaccia che può essere utilizzato per la fase di esecuzione del polimorfismo (f.ex. eterogenei i contenitori). Questi sono utili, non solo per le stesse attività. Alcune classi ereditano sia da una classe base che è un tipo di interfaccia per il polimorfismo a runtime e altro che un momento della compilazione del modello di helper.

    OriginaleL’autore stands2reason

  12. -3

    Non sto vedendo l’importanza del virtuale parola chiave qui. b è uno statico digitato variabile, e il suo tipo è determinato dal compilatore in fase di compilazione. Le chiamate di funzione non di riferimento vtable. Quando b è costruito, la sua classe padre del costruttore viene chiamato, che è il motivo per cui il valore di _n è impostato a 1.

    La domanda è perché b‘s costruttore chiama la base f(), non derivata ignorare. Tipo della variabile b è irrilevante.
    “Le chiamate di funzione non di riferimento vtable” Che non è vero. Se si pensa di invio virtuale è abilitato solo quando si accede attraverso un B* o B&`, vi sbagliate.
    A parte il fatto che segue una sua logica conclusione sbagliata… L’idea alla base di questa risposta, noto tipo statico, è erroneamente applicata. Un compilatore potrebbe devirtualise b.getN() perché conosce il tipo reale, & solo direttamente di spedizione per la versione da B. Ma è solo un indennità di fatto dal se regola. Tutto ancora deve agire se virtual table & seguita alla lettera. Nel A costruttore, lo stesso è vero: anche se (probabilmente non è possibile) si ottiene inline w/ il B ctor, la chiamata virtuale deve ancora agire se ha solo la base A vtable disponibile per l’uso.
    Puoi farmi un esempio per la sua affermazione che virtuale spedizione avviene senza chiamare attraverso un riferimento o puntatore (implicita this)?
    Hai ragione a dire che la chiamata b.getn() non è virtuale. b è un staticamente tipizzato oggetto, e tutto ciò getn() è definito per il suo tipo di chiamata. Ma all’interno di funzioni membro, compreso il costruttore, tutti i membri chiamate di funzione implicita this puntatore e quindi sono virtuali chiamate di funzione, se si tratta di un polimorfici classe. La ragione e la logica per risolvere il virtuale fn() chiamata della classe base dell’attuazione — anche se avviene durante la costruzione complessiva di un oggetto derivato-è spiegato in altre risposte.

    OriginaleL’autore user2305329

  13. -4

    Durante il costruttore di un oggetto di chiamare la funzione virtuale puntatore tabella non è completamente costruito. Facendo questo di solito non ti danno il comportamento desiderato. La chiamata di una funzione virtuale in questa situazione può funzionare, ma non garantite e dovrebbero essere evitati per essere portatile e seguire il C++ standard.

    la Chiamata di una funzione virtuale, in questa situazione, potrebbe funzionare, ma non è garantitonon corretto. Il comportamento è garantito.
    chiamare la versione base, se disponibile, o per richiamare UB se il vfunc è pure virtuale.

    OriginaleL’autore terry

Lascia un commento