Chiamare una funzione virtuale dal costruttore

Sto leggendo Effective C ++ e c’è “Item 9: Never call virtual function in construction and destruction”. E mi chiedo se il mio codice va bene anche se infrange questa regola:

using namespace std; class A{ public: A(bool doLog){ if(doLog) log(); } virtual void log(){ cout << "logging A\n"; } }; class B: public A{ public: B(bool doLog) : A(false){ if(doLog) log(); } virtual void log(){ cout << "logging B\n"; } }; int main() { A a(true); B b(true); } 

C’è qualcosa di sbagliato in questo approccio? Posso finire nei guai quando faccio qualcosa di più complicato?

Mi sembra che la maggior parte delle risposte non abbia ottenuto ciò che ho fatto lì, e hanno semplicemente spiegato di nuovo perché chiamare la funzione virtuale dal costruttore potenzialmente pericoloso.

Vorrei sottolineare che l’output del mio programma assomiglia a questo:

 logging A logging B 

Così ottengo A loggato quando è costruito e B registrato quando è costruito. Ed è quello che voglio ! Ma ti sto chiedendo se trovi qualcosa di sbagliato (potenzialmente pericoloso) con il mio “hack” per superare il problema con il richiamo della funzione virtuale nel costruttore.

    E mi chiedo se il mio codice va bene anche se infrange questa regola:

    Dipende da cosa intendi per “bene”. Il tuo programma è ben formato e il suo comportamento è ben definito, quindi non invocherà comportamenti indefiniti e cose del genere.

    Tuttavia, quando si visualizza una chiamata a una funzione virtuale, è ansible che la chiamata venga risolta richiamando l’implementazione fornita dal tipo più derivato che sovrascrive quella funzione.

    Tranne che durante la costruzione, il corrispondente object secondario non è stato ancora costruito, quindi il suboject più derivato è quello attualmente in costruzione. Risultato: la chiamata viene inviata come se la funzione non fosse virtuale.

    Questo è contro-intuitivo e il tuo programma non dovrebbe fare affidamento su questo comportamento. Pertanto, come programmatore esperto, dovresti abituarti a evitare un simile schema e seguire le linee guida di Scott Meyer.

    C’è qualcosa di sbagliato in questo approccio?

    Risposta di Bjarne Stroustrup:

    Posso chiamare una funzione virtuale da un costruttore?

    Sì, ma attenzione. Potrebbe non fare quello che ti aspetti. In un costruttore, il meccanismo di chiamata virtuale è disabilitato perché l’override delle classi derivate non è ancora avvenuta. Gli oggetti sono costruiti dalla base in su, “base prima derivata”. Prendere in considerazione:

      #include #include using namespace std; class B { public: B(const string& ss) { cout << "B constructor\n"; f(ss); } virtual void f(const string&) { cout << "B::f\n";} }; class D : public B { public: D(const string & ss) :B(ss) { cout << "D constructor\n";} void f(const string& ss) { cout << "D::f\n"; s = ss; } private: string s; }; int main() { D d("Hello"); } 

    il programma compila e produce

     B constructor B::f D constructor 

    Nota non D :: f. Considerare cosa accadrebbe se la regola fosse diversa in modo che D :: f () fosse chiamato da B :: B (): poiché il costruttore D :: D () non era ancora stato eseguito, D :: f () sarebbe prova ad assegnare il suo argomento a una stringa non inizializzata s. Il risultato sarebbe molto probabilmente un incidente immediato. La distruzione viene eseguita "class derivata prima della class base", quindi le funzioni virtuali si comportano come nei costruttori: vengono utilizzate solo le definizioni locali e non vengono effettuate chiamate alle funzioni di override per evitare di toccare la parte della class derivata (ora distrutta) dell'object.

    Per maggiori dettagli vedere D & E 13.2.4.2 o TC ++ PL3 15.4.3.

    È stato suggerito che questa regola sia un artefatto di implementazione. Non è così. In effetti, sarebbe notevolmente più semplice implementare la regola non sicura di chiamare le funzioni virtuali dai costruttori esattamente come da altre funzioni. Tuttavia, ciò implicherebbe che nessuna funzione virtuale potrebbe essere scritta per fare affidamento su invarianti stabiliti dalle classi base. Sarebbe un disastro terribile.

    È “bello” nel senso di essere ben definito. Potrebbe non essere “buono” nel senso di fare ciò che ti aspetti.

    Chiamerai l’override dalla class attualmente in costruzione (o distrutta), non l’override finale; poiché la class derivata finale non è stata ancora costruita (o è già stata distrutta) e quindi non è ansible accedervi. Quindi potresti metterti nei guai se vuoi che l’override finale venga chiamato qui.

    Poiché questo comportamento è potenzialmente confuso, è meglio evitare di doverlo fare. Raccomanderei di aggiungere un comportamento ad una class tramite aggregazione piuttosto che la sottoclass in quella situazione; i membri della class sono costruiti prima del corpo del costruttore e durano fino a dopo il distruttore, quindi sono disponibili in entrambi i luoghi.

    Una cosa che non devi fare è chiamare una funzione virtuale dal costruttore o dal distruttore se è pura virtuale in quella class; è un comportamento indefinito.