2013-05-23 13 views
6

Czytam Effective C++, a tam jest "Element 9: Nigdy nie wywołuj funkcji wirtualnych podczas budowy lub zniszczenia". I zastanawiam się, czy mój kod jest w porządku, nawet jeśli złamie tę zasadę:Wywołanie funkcji wirtualnej od konstruktora

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

Czy jest coś nie tak z tym podejściem? Czy mogę wpaść w kłopoty, gdy zrobię coś bardziej skomplikowanego?

To szokuje, że większość odpowiedzi nie dostała tego, co tam zrobiłem, i po prostu wyjaśniła ponownie, dlaczego wywołuje funkcję wirtualną od konstruktora potencjalnie niebezpiecznego.

Chciałbym podkreślić, że wyjście z mojego programu wygląda następująco:

logging A 
logging B 

Więc dostać zalogowany, gdy jest on skonstruowany i B zalogowany, gdy jest on zbudowany. I to właśnie chcę chce! Ale pytam, czy znajdziesz coś złego (potencjalnie niebezpiecznego) z moim "hack", aby przezwyciężyć problem wywoływania funkcji wirtualnej w konstruktorze.

+1

W tym przypadku załatałeś to dobrze, ale jeśli inne osoby będą musiały użyć tego kodu, błędy będą musiały się zdarzyć. –

+2

@JoachimPileborg to nie jest prawda: zachowanie * jest * zdefiniowane. Podczas budowy wirtualne wywołania funkcji są wyłączone (np. Używana jest implementacja obecnie konstruującego typu). –

Odpowiedz

11

I zastanawiam się, czy mój kod jest w porządku, nawet jeśli łamie tę zasadę:

To zależy co masz na myśli przez „fine”. Twój program jest dobrze sformułowany, a jego zachowanie jest dobrze zdefiniowane, więc nie będzie wywoływać niezdefiniowanych zachowań i takich rzeczy.

Jednak można oczekiwać, że podczas wywoływania wywołania funkcji wirtualnej wywołanie zostanie rozstrzygnięte przez wywołanie implementacji dostarczonej przez najbardziej wyprowadzony typ, który przesłania tę funkcję.

Z wyjątkiem tego, że podczas konstrukcji odpowiedni pod-obiekt nie został jeszcze skonstruowany, więc najbardziej pochodnym obiektem podrzędnym jest ten, który jest aktualnie tworzony. Wynik: wywołanie jest wywoływane tak, jakby funkcja nie była wirtualna.

Jest to sprzeczne z intuicją, a Twój program nie powinien polegać na tym zachowaniu. Dlatego też, jako doświadczony programista, powinieneś się przyzwyczaić, aby uniknąć takiego schematu i postępować zgodnie ze wskazówkami Scotta Meyera.

+0

Ładnie wyjaśniono, ale muszę powiedzieć, że jest to bardzo intuicyjne. Programowałem w C++ od lat i kiedy później odkryłem języki, które nazywałem najbardziej wersją klas, byłem wstrząśnięty i przerażony. – Steve

15

Czy jest coś nie tak z tym podejściem?

Odpowiedź z Bjarne Stroustrup:

mogę wywołać funkcję wirtualnego z konstruktora?

Tak, ale bądź ostrożny. Może nie robić tego, czego się spodziewasz. W konstruktorze, mechanizm wywoływania wirtualnego jest wyłączony, ponieważ przesłanianie z wyprowadzonych klas jeszcze się nie stało. Obiekty są konstruowane z podstawy, "baza przed wyprowadzeniem". Rozważmy:

#include<string> 
    #include<iostream> 
    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"); 
} 

program kompiluje i produkować

B constructor 
B::f 
D constructor 

Uwaga nie D :: f. Zastanówmy się, co by się stało, gdyby reguła była inna, więc D :: f() został wywołany z B :: B(): Ponieważ konstruktor D :: D() jeszcze nie został uruchomiony, D :: f() spróbuj przypisać jego argument do niezainicjowanego ciągu s. Rezultatem najprawdopodobniej byłaby natychmiastowa awaria. Zniszczenie jest wykonywane "klasa pochodna przed klasą podstawową", więc funkcje wirtualne zachowują się tak, jak w konstruktorach: używane są tylko definicje lokalne - i nie są wywoływane funkcje nadpisujące, aby uniknąć dotykania (teraz zniszczonej) pochodnej części klasy obiektu. Aby uzyskać więcej informacji zobacz D & E 13.2.4.2 lub TC++ PL3 15.4.3.

Zasugerowano, że ta reguła jest artefaktem implementacji. To nie jest takie. W rzeczywistości łatwiej byłoby wdrożyć niebezpieczną regułę wywoływania funkcji wirtualnych od konstruktorów dokładnie tak, jak od innych funkcji. Jednakże oznaczałoby to, że nie można napisać żadnej funkcji wirtualnej, aby polegać na niezmiennikach ustalonych przez klasy bazowe. To byłby okropny bałagan.

4

Jest "w porządku" w sensie bycia dobrze zdefiniowanym. Może nie być "w porządku" w sensie robienia tego, czego się spodziewasz.

Możesz wywołać zastąpienie klasy, która obecnie jest tworzona (lub niszczona), a nie ostateczne nadpisanie; ponieważ ostatnia klasa pochodna nie została jeszcze zbudowana (lub została już zniszczona) i dlatego nie można uzyskać do niej dostępu. Więc możesz wpaść w tarapaty, jeśli chcesz, aby tu nadano ostateczne zastąpienie.

Ponieważ takie zachowanie jest potencjalnie mylące, najlepiej unikać tego. Poleciłabym dodać zachowanie do klasy przez agregację zamiast podklasy w tej sytuacji; członkowie klasy są konstruowani przed ciałem konstruktora, a ostatni do destruktora, a więc są dostępne w obu tych miejscach.

Jedną rzeczą, której nie wolno wykonywać, jest wywołanie funkcji wirtualnej z konstruktora lub destruktora, jeśli jest ona czysto wirtualna w tej klasie; to jest niezdefiniowane zachowanie.

Powiązane problemy