Czy istnieje kiedykolwiek dobry powód, aby nie deklarować wirtualnego destruktora dla klasy? Kiedy w szczególności należy unikać pisania?
Czy istnieje kiedykolwiek dobry powód, aby nie deklarować wirtualnego destruktora dla klasy? Kiedy w szczególności należy unikać pisania?
Odpowiedzi:
Nie ma potrzeby używania wirtualnego destruktora, jeśli spełniony jest którykolwiek z poniższych warunków:
Nie ma konkretnego powodu, aby tego uniknąć, chyba że naprawdę zależy ci na pamięci.
Aby jednoznacznie odpowiedzieć na pytanie, czyli kiedy nie należy deklarować wirtualnego destruktora.
C ++ '98 / '03
Dodanie wirtualnego destruktora może zmienić twoją klasę z POD (zwykłe stare dane) * lub zagregować na inną niż POD. Może to uniemożliwić kompilację projektu, jeśli typ Twojej klasy jest gdzieś zainicjowany jako agregacja.
struct A {
// virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // Will fail if virtual dtor declared
}
W skrajnym przypadku taka zmiana może również spowodować niezdefiniowane zachowanie, gdy klasa jest używana w sposób wymagający POD, np. Przekazanie jej przez parametr wielokropka lub użycie jej z memcpy.
void bar (...);
void foo (A & a) {
bar (a); // Undefined behavior if virtual dtor declared
}
[* Typ POD jest typem, który ma określone gwarancje dotyczące układu pamięci. Standard tak naprawdę mówi tylko, że jeśli miałbyś skopiować z obiektu typu POD do tablicy znaków (lub znaków bez znaku) iz powrotem, to wynik będzie taki sam, jak oryginalny obiekt.]
Nowoczesny C ++
W ostatnich wersjach C ++ koncepcja POD została podzielona na układ klasy oraz jej konstrukcję, kopiowanie i niszczenie.
W przypadku wielokropka nie jest to już niezdefiniowane zachowanie, jest teraz warunkowo obsługiwane za pomocą semantyki zdefiniowanej w implementacji (N3937 - ~ C ++ '14 - 5.2.2 / 7):
... Przekazanie potencjalnie ocenianego argumentu typu klasy (klauzula 9) posiadającego nietrywialny konstruktor kopiujący, nietrywialny konstruktor przenoszenia lub trywialny destruktor, bez odpowiadającego mu parametru, jest warunkowo obsługiwane przez implementację- zdefiniowana semantyka.
Zadeklarowanie destruktora innego niż =default
oznacza, że nie jest to trywialne (12.4 / 5)
... Destruktor jest trywialny, jeśli nie jest dostarczony przez użytkownika ...
Inne zmiany w nowoczesnym C ++ zmniejszają wpływ problemu inicjalizacji agregacji, ponieważ można dodać konstruktor:
struct A {
A(int i, int j);
virtual ~A ();
int i;
int j;
};
void foo () {
A a = { 0, 1 }; // OK
}
Deklaruję wirtualny destruktor wtedy i tylko wtedy, gdy mam wirtualne metody. Kiedy już mam metody wirtualne, nie ufam sobie, że uniknę tworzenia ich instancji na stercie lub przechowywania wskaźnika do klasy bazowej. Obie te operacje są niezwykle powszechne i często powodują dyskretny wyciek zasobów, jeśli destruktor nie zostanie zadeklarowany jako wirtualny.
Wirtualny destruktor jest potrzebny zawsze, gdy istnieje jakakolwiek szansa, że delete
można go wywołać na wskaźniku do obiektu podklasy z typem Twojej klasy. Daje to pewność, że w czasie wykonywania zostanie wywołany właściwy destruktor bez konieczności znajomości przez kompilator klasy obiektu na stercie w czasie kompilacji. Na przykład, załóżmy, że B
jest podklasą A
:
A *x = new B;
delete x; // ~B() called, even though x has type A*
Jeśli twój kod nie ma krytycznego znaczenia dla wydajności, rozsądne byłoby dodanie wirtualnego destruktora do każdej klasy bazowej, którą piszesz, tylko dla bezpieczeństwa.
Jeśli jednak znajdziesz delete
wiele obiektów w ciasnej pętli, narzut wydajności związany z wywołaniem funkcji wirtualnej (nawet takiej, która jest pusta) może być zauważalny. Kompilator zwykle nie może wbudować tych wywołań, a procesor może mieć trudności z przewidzeniem, dokąd się udać. Jest mało prawdopodobne, aby miało to znaczący wpływ na wydajność, ale warto o tym wspomnieć.
Funkcje wirtualne oznaczają, że każdy przydzielony obiekt zwiększa koszt pamięci o wskaźnik tablicy funkcji wirtualnych.
Więc jeśli twój program wymaga przydzielenia bardzo dużej liczby obiektów, warto byłoby unikać wszystkich funkcji wirtualnych, aby zaoszczędzić dodatkowe 32 bity na obiekt.
We wszystkich innych przypadkach oszczędzisz sobie kłopotów związanych z debugowaniem, aby uczynić dtor wirtualnym.
Nie wszystkie klasy C ++ nadają się do użytku jako klasa bazowa z dynamicznym polimorfizmem.
Jeśli chcesz, aby Twoja klasa nadawała się do dynamicznego polimorfizmu, jej destruktor musi być wirtualny. Ponadto wszelkie metody, które podklasa mogłaby chcieć przesłonić (co może oznaczać wszystkie metody publiczne oraz potencjalnie niektóre chronione używane wewnętrznie) muszą być wirtualne.
Jeśli twoja klasa nie nadaje się do dynamicznego polimorfizmu, to destruktor nie powinien być oznaczony jako wirtualny, ponieważ jest to mylące. Po prostu zachęca ludzi do niewłaściwego używania twojej klasy.
Oto przykład klasy, która nie byłaby odpowiednia dla dynamicznego polimorfizmu, nawet gdyby jej destruktor był wirtualny:
class MutexLock {
mutex *mtx_;
public:
explicit MutexLock(mutex *mtx) : mtx_(mtx) { mtx_->lock(); }
~MutexLock() { mtx_->unlock(); }
private:
MutexLock(const MutexLock &rhs);
MutexLock &operator=(const MutexLock &rhs);
};
Celem tej klasy jest siedzenie na stosie dla RAII. Jeśli przekazujesz wskazówki do obiektów tej klasy, nie mówiąc już o jej podklasach, to robisz to źle.
Dobrym powodem, aby nie deklarować destruktora jako wirtualnego, jest sytuacja, gdy chroni to twoją klasę przed dodaniem tablicy funkcji wirtualnych i powinieneś tego unikać, gdy tylko jest to możliwe.
Wiem, że wiele osób woli po prostu zawsze deklarować destruktory jako wirtualne, aby być po bezpiecznej stronie. Ale jeśli twoja klasa nie ma żadnych innych funkcji wirtualnych, to naprawdę nie ma sensu mieć wirtualnego destruktora. Nawet jeśli dasz swoją klasę innym ludziom, którzy następnie wyprowadzą z niej inne klasy, nie będą mieli powodu, aby kiedykolwiek wywoływać funkcję delete na wskaźniku, który był upcastowany do Twojej klasy - a jeśli tak, to uznałbym to za błąd.
W porządku, jest jeden wyjątek, a mianowicie, jeśli twoja klasa jest (niewłaściwie) używana do wykonywania polimorficznego usuwania obiektów pochodnych, ale wtedy ty - lub inni faceci - miejmy nadzieję, że wiecie, że wymaga to wirtualnego destruktora.
Innymi słowy, jeśli twoja klasa ma niewirtualny destruktor, to jest to bardzo jasne stwierdzenie: „Nie używaj mnie do usuwania obiektów pochodnych!”
Zwykle deklaruję wirtualny destruktor, ale jeśli masz kod krytyczny dla wydajności, który jest używany w pętli wewnętrznej, możesz chcieć uniknąć wyszukiwania wirtualnej tabeli. Może to być ważne w niektórych przypadkach, takich jak sprawdzanie kolizji. Ale uważaj na to, jak zniszczysz te obiekty, jeśli użyjesz dziedziczenia, w przeciwnym razie zniszczysz tylko połowę obiektu.
Zwróć uwagę, że wyszukiwanie w tabeli wirtualnej ma miejsce dla obiektu, jeśli jakakolwiek metoda na tym obiekcie jest wirtualna. Dlatego nie ma sensu usuwać wirtualnej specyfikacji destruktora, jeśli w klasie są inne metody wirtualne.
Jeśli absolutnie koniecznie musisz upewnić się, że twoja klasa nie ma tabeli vtable, nie możesz również mieć wirtualnego destruktora.
To rzadki przypadek, ale się zdarza.
Najbardziej znanym przykładem wzorca, który to robi, są klasy DirectX D3DVECTOR i D3DMATRIX. Są to metody klas zamiast funkcji dla cukru składniowego, ale klasy celowo nie mają tabeli vtable, aby uniknąć narzutu funkcji, ponieważ te klasy są specjalnie używane w pętli wewnętrznej wielu aplikacji o wysokiej wydajności.
W przypadku operacji, która zostanie wykonana na klasie bazowej i która powinna działać wirtualnie, powinna być wirtualna. Jeśli usunięcie można przeprowadzić polimorficznie za pośrednictwem interfejsu klasy bazowej, to musi zachowywać się wirtualnie i być wirtualne.
Destruktor nie musi być wirtualny, jeśli nie zamierzasz wywodzić się z klasy. A nawet jeśli to zrobisz, chroniony niewirtualny destruktor jest równie dobry, jeśli usunięcie wskaźników klasy bazowej nie jest wymagane .
Odpowiedź na przedstawienie jest jedyną, jaką znam, a która ma szansę być prawdziwa. Jeśli zmierzyłeś i odkryłeś, że de-wirtualizacja twoich destruktorów naprawdę przyspiesza rzeczy, to prawdopodobnie masz inne rzeczy w tej klasie, które również wymagają przyspieszenia, ale w tym momencie są ważniejsze kwestie. Któregoś dnia ktoś odkryje, że Twój kod zapewniłby im niezłą klasę bazową i zaoszczędziłby im tygodniowej pracy. Lepiej upewnij się, że wykonują pracę w tym tygodniu, kopiując i wklejając kod, zamiast używać go jako podstawy. Lepiej upewnij się, że niektóre z twoich ważnych metod są prywatne, aby nikt nigdy nie mógł po tobie odziedziczyć.