Kiedy nie należy używać wirtualnych destruktorów?

99

Czy istnieje kiedykolwiek dobry powód, aby nie deklarować wirtualnego destruktora dla klasy? Kiedy w szczególności należy unikać pisania?

Mag Roader
źródło

Odpowiedzi:

72

Nie ma potrzeby używania wirtualnego destruktora, jeśli spełniony jest którykolwiek z poniższych warunków:

  • Brak zamiaru wyprowadzania z niego klas
  • Brak instancji na stercie
  • Brak zamiaru przechowywania we wskaźniku nadklasy

Nie ma konkretnego powodu, aby tego uniknąć, chyba że naprawdę zależy ci na pamięci.

wrz
źródło
25
To nie jest dobra odpowiedź. „Nie ma potrzeby” różni się od „nie powinno”, a „brak intencji” różni się od „niemożliwego”.
Programista Windows,
5
Dodaj również: brak zamiaru usuwania instancji za pomocą wskaźnika klasy bazowej.
Adam Rosenfield,
9
To naprawdę nie odpowiada na pytanie. Gdzie jest twój dobry powód, aby nie używać wirtualnego dtora?
mxcl
9
Myślę, że jeśli nie ma takiej potrzeby, to dobry powód, żeby tego nie robić. Jest zgodny z zasadą Simple Design systemu XP.
wrz
12
Mówiąc, że nie masz zamiaru, robisz ogromne założenie co do tego, jak Twoja klasa zostanie wykorzystana. Wydaje mi się, że najprostszym rozwiązaniem w większości przypadków (które powinno być domyślne) powinno być posiadanie wirtualnych destruktorów i unikanie ich tylko wtedy, gdy masz określony powód, aby tego nie robić. Więc wciąż jestem ciekawy, jaki byłby dobry powód.
ckarras
68

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ż =defaultoznacza, ż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
}
Richard Corden
źródło
1
Masz rację, myliłem się, wydajność nie jest jedynym powodem. Ale to pokazuje, że miałem rację co do reszty: programista klasy powinien lepiej dołączyć kod, aby klasa nigdy nie była dziedziczona przez kogokolwiek innego.
Programista Windows
drogi Ryszardzie, czy możesz skomentować trochę więcej tego, co napisałeś. Nie rozumiem twojego punktu, ale wydaje mi się, że jest to jedyna cenna kwestia, którą znalazłem w Google) A może możesz podać link do bardziej szczegółowego wyjaśnienia?
John Smith
1
@JohnSmith Zaktualizowałem odpowiedź. Mam nadzieję, że to pomoże.
Richard Corden
28

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.

Andy
źródło
3
W rzeczywistości istnieje opcja ostrzegawcza w gcc, która ostrzega dokładnie o tym przypadku (metody wirtualne, ale nie wirtualny dtor).
CesarB
6
Czy nie ryzykujesz wycieku pamięci, jeśli wywodzisz się z klasy, niezależnie od tego, czy masz inne funkcje wirtualne?
Mag Roader,
1
Zgadzam się z mag. Takie użycie wirtualnego destruktora i / lub wirtualnej metody to oddzielne wymagania. Wirtualny destruktor umożliwia klasom czyszczenie (np. Usuwanie pamięci, zamykanie plików itp.) ORAZ zapewnia również wywołanie konstruktorów wszystkich jej członków.
user48956
7

Wirtualny destruktor jest potrzebny zawsze, gdy istnieje jakakolwiek szansa, że deletemoż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 Bjest 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 deletewiele 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ć.

Jay Conrod
źródło
„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”. powinienem być bardziej podkreślony w każdej odpowiedzi, którą widzę
csguy,
5

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.

mxcl
źródło
1
Po prostu szukanie nitek, ale obecnie wskaźnik często będzie miał 64 bity zamiast 32.
Head Geek
5

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.

Steve Jessop
źródło
2
Użycie polimorficzne nie oznacza delecji polimorficznej. Istnieje wiele przypadków użycia klasy, które mogą mieć metody wirtualne, ale nie ma wirtualnego destruktora. Rozważmy typowe statycznie zdefiniowane okno dialogowe w prawie każdym zestawie narzędzi GUI. Okno nadrzędne zniszczy obiekty podrzędne i zna dokładny typ każdego z nich, ale wszystkie okna podrzędne będą również używane polimorficznie w dowolnej liczbie miejsc, takich jak testowanie trafień, rysowanie, API dostępności, które pobierają tekst dla tekstu- silniki mowy itp.
Ben Voigt,
4
To prawda, ale pytający pyta, kiedy konkretnie należy unikać wirtualnego destruktora. W opisywanym oknie dialogowym wirtualny destruktor nie ma sensu, ale IMO nie jest szkodliwy. Nie jestem pewien, czy byłbym pewien, że nigdy nie będę musiał usuwać okna dialogowego za pomocą wskaźnika klasy bazowej - na przykład mogę w przyszłości chcieć, aby moje okno nadrzędne tworzyło swoje obiekty podrzędne za pomocą fabryk. Nie chodzi więc o unikanie wirtualnego destruktora, tylko o to, żebyś nie zawracał sobie głowy jego posiadaniem. Wirtualny destruktor klasy, która nie nadaje się do wyprowadzenia, jest jednak szkodliwy, ponieważ wprowadza w błąd.
Steve Jessop
4

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!”

kidfisto
źródło
3

Jeśli masz bardzo małą klasę z ogromną liczbą instancji, obciążenie wskaźnika vtable może mieć wpływ na wykorzystanie pamięci programu. Dopóki twoja klasa nie ma żadnych innych metod wirtualnych, uczynienie destruktora niewirtualnego oszczędza ten narzut.

Mark Okup
źródło
1

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.

Jørn Jensen
źródło
1

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.

Lisa
źródło
0

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 .

przestępczość lodowa
źródło
-7

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ć.

Programista Windows
źródło
Polimorfizm z pewnością spowolni. Porównaj to z sytuacją, w której potrzebujemy polimorfizmu i zdecydujemy się tego nie robić, będzie jeszcze wolniej. Przykład: implementujemy całą logikę w destruktorze klasy bazowej, używając RTTI i instrukcji switch do czyszczenia zasobów.
wrz
1
W C ++ nie jesteś odpowiedzialny za powstrzymanie mnie przed dziedziczeniem z twoich klas, które udokumentowałeś, że nie nadają się do użycia jako klasy bazowe. Moim obowiązkiem jest ostrożność w dziedziczeniu. O ile przewodnik po stylu domu nie mówi inaczej, oczywiście.
Steve Jessop
1
... samo uczynienie destruktora wirtualnym nie oznacza, że ​​klasa musi działać poprawnie jako klasa bazowa. Oznaczanie go jako wirtualnego „tylko dlatego, że” zamiast dokonać oceny oznacza wypisanie czeku, którego mój kod nie może zrealizować.
Steve Jessop