Ostatnio wzięło mnie na prototypowanie swoich starych bibliotek. Chciałem zobaczyć które z moich starych bibliotek, które pisałem jako młody koder jeszcze nadają się do użytku, a głębi tego miałem zamiar posprawdzać parę rzeczy nt. C++, na które nigdy nie miałem czasu. Jedną z bardziej zaskakujących jest dziedziczenie połączone z polimorfizmem i destruktorem.
Sprawa wyglądała dosyć prosto:
class CRef { public: virtual ~CRef() { std::cout << "Destructor Pierwszy" << std::endl; Free(); } virtual void Free() { std::cout << "Czyszczenie Pierwsze" << std::endl; } }; class CSomeClass : public CRef { public: virtual ~CSomeClass() { std::cout << "Destructor Drugi" << std::endl; } virtual void Free() { std::cout << "Czyszczenie Drugie" << std::endl; } };
Mamy klasy z destruktorami, gdzie pierwszy wywołuje metodę czyszczącą. W zamyśle, gdy skasujemy obiekt klasy drugiej, to klasa bazowa wywoła przez destruktor metodę czyszczącą klasy wyższej – proste? Oczywiście. Działa? Oczywiście, że nie….
Wynik jaki powinienem otrzymać:
Destruktor Pierwszy Czyszczenie Drugie Destruktor Drugi
Co otrzymałem:
Destruktor Pierwszy Czyszczenie Pierwsze Destruktor Drugi
To, że wywołują się oba destruktory, jest zjawiskiem normalnym i mnie nawet nie zainteresowało. Delikatnie mnie zdziwiła za to inna sytuacja, że wywoła się metoda klasy bazowej, a powielone testy tylko utwierdziły mnie w przekonaniu, że dany kod nie ma prawa działać poprawnie. Destruktor jest typem metody, w której możemy uruchamiać jedynie metody klasy bazowej – wymuszenie polimorfizmu poprzez zerowanie metody (virtual void Free() = 0;) w tej klasie prowadzi jedynie do błędu linkera.
Innymi słowy – jedyny sposób, żeby polimorfizm działał poprawnie w takim wypadku, jest wywołanie metody Free(); przez inną, normalnie stworzoną metodę.
class CRef { public: virtual ~CRef() { std::cout << "Destructor Pierwszy" << std::endl; } virtual void Free() { std::cout << "Czyszczenie Pierwsze" << std::endl; } void Release() { Free(); } }; class CSomeClass : public CRef { public: virtual ~CSomeClass() { std::cout << "Destructor Drugi" << std::endl; } virtual void Free() { std::cout << "Czyszczenie Drugie" << std::endl; } }; ... pClass->Release(); delete pClass; ...
Taki kod już się wykona się już poprawnie, a wywołana zostanie metoda klasy dziedziczącej. Trochę to wkurzające, ale nic co nie da się ominąć paroma sprytnymi makrami
Rzeczywiście, jakiś czas temu napotkałem na ten problem. Okazuje się, że tylko przy specyficznych ustawieniach kompilator VC++ to łyka, co nie zmienia faktu, że to nie jest zgodne ze standardem. Postaram się w najbliższym czasie to sprostować w następnym artykule i opisać najprostsze obejście tego problemu.
W każdym razie, dziękuje za konstruktywny komentarz.
Nie jestem w żadnym wypadku ekspertem w tej dziedzinie, a dokładniej dopiero się wdrażam w C++, ale w (podobno świetnej i jestem w stanie się z tym zgodzić) książce Mayersa “Effective C++” jest nawet rozdział o tytule “Never call virtual functions during construction or destruction”. Jest w nim wytłumaczone (nie będę przytaczał, opowiadanie tego we własnych słowach nie oddałoby sensu oryginału) dlaczego działa to tak jak działa, dlaczego nie należy w konstruktorach/destruktorach wywoływać metod wirtualnych (ani żadnych, które wywołują funkcje wirtualne(!), jak w powyższym hacku) i, co najważniejsze, jak warto do tego podchodzić.
Polecam i pozdrawiam!