Dlaczego powinienem zadeklarować wirtualny destruktor dla klasy abstrakcyjnej w C ++?

165

Wiem, że dobrą praktyką jest deklarowanie wirtualnych destruktorów dla klas bazowych w C ++, ale czy zawsze ważne jest deklarowanie virtualdestruktorów nawet dla klas abstrakcyjnych, które działają jako interfejsy? Proszę podać powody i przykłady.

Kevin
źródło

Odpowiedzi:

196

Jest to jeszcze ważniejsze w przypadku interfejsu. Każdy użytkownik twojej klasy prawdopodobnie będzie trzymał wskaźnik do interfejsu, a nie wskaźnik do konkretnej implementacji. Kiedy przyjdą, aby go usunąć, jeśli destruktor nie jest wirtualny, będą wywoływać destruktor interfejsu (lub wartość domyślną dostarczoną przez kompilator, jeśli go nie określono), a nie destruktor klasy pochodnej. Natychmiastowy wyciek pamięci.

Na przykład

class Interface
{
   virtual void doSomething() = 0;
};

class Derived : public Interface
{
   Derived();
   ~Derived() 
   {
      // Do some important cleanup...
   }
};

void myFunc(void)
{
   Interface* p = new Derived();
   // The behaviour of the next line is undefined. It probably 
   // calls Interface::~Interface, not Derived::~Derived
   delete p; 
}
Airsource Ltd
źródło
4
delete pwywołuje niezdefiniowane zachowanie. Nie ma gwarancji, że zadzwonisz Interface::~Interface.
Mankarse
@Mankarse: czy możesz wyjaśnić, dlaczego jest niezdefiniowany? Gdyby Derived nie zaimplementował własnego destruktora, czy nadal byłoby to niezdefiniowane zachowanie?
Ponkadoodle
14
@Wallacoloo: To jest niezdefiniowany z powodu [expr.delete]/: ... if the static type of the object to be deleted is different from its dynamic type, ... the static type shall have a virtual destructor or the behavior is undefined. .... Byłoby nadal niezdefiniowane, gdyby funkcja Derived używała niejawnie wygenerowanego destruktora.
Mankarse
37

Odpowiedź na twoje pytanie brzmi często, ale nie zawsze. Jeśli twoja klasa abstrakcyjna zabrania klientom wywoływania funkcji delete na wskaźniku do niej (lub jeśli tak mówi w swojej dokumentacji), możesz nie deklarować wirtualnego destruktora.

Możesz zabronić klientom wywoływania funkcji delete na wskaźniku do niego, chroniąc jego destruktor. Działając w ten sposób, pominięcie wirtualnego destruktora jest całkowicie bezpieczne i rozsądne.

Ostatecznie skończysz bez wirtualnej tabeli metod i w końcu zasygnalizujesz swoim klientom zamiar uczynienia jej niemożliwą do usunięcia przez wskaźnik do niej, więc naprawdę masz powód, aby nie deklarować jej jako wirtualnej w takich przypadkach.

[Patrz punkt 4 w tym artykule: http://www.gotw.ca/publications/mill18.htm ]

Johannes Schaub - litb
źródło
Kluczem do tego, aby Twoja odpowiedź działała, jest to, „w przypadku której nie jest wymagane usuwanie”. Zwykle, jeśli masz abstrakcyjną klasę bazową zaprojektowaną jako interfejs, metoda delete zostanie wywołana w klasie interfejsu.
John Dibling
Jak John powyżej wskazał, to, co sugerujesz, jest dość niebezpieczne. Opierasz się na założeniu, że klienci twojego interfejsu nigdy nie zniszczą obiektu znającego tylko typ podstawowy. Jedynym sposobem, w jaki możesz zagwarantować, że jeśli nie jest to wirtualne, jest zabezpieczenie dtora klasy abstrakcyjnej.
Michel
Michel, powiedziałem tak :) „Jeśli to zrobisz, ochronisz swój destruktor. Jeśli to zrobisz, klienci nie będą mogli usuwać za pomocą wskaźnika do tego interfejsu.” i rzeczywiście nie polega na klientach, ale musi wymusić to, mówiąc klientom „nie możesz tego zrobić…”. Nie widzę żadnego niebezpieczeństwa
Johannes Schaub - litb
Poprawiłem teraz słabe brzmienie mojej odpowiedzi. teraz wyraźnie stwierdza, że ​​nie polega na klientach. Właściwie to myślałem, że to oczywiste, że poleganie na klientach, którzy coś robią, i tak nie jest możliwe. dzięki :)
Johannes Schaub - litb
2
+1 za wzmiankę o chronionych destruktorach, które są kolejnym „wyjściem” z problemu przypadkowego wywołania niewłaściwego destruktora podczas usuwania wskaźnika do klasy bazowej.
j_random_hacker,
23

Postanowiłem poszukać informacji i podsumować Twoje odpowiedzi. Poniższe pytania pomogą Ci zdecydować, jakiego rodzaju destruktora potrzebujesz:

  1. Czy Twoja klasa ma być używana jako klasa bazowa?
    • Nie: zadeklaruj publiczny niewirtualny destruktor, aby uniknąć wskaźnika v na każdym obiekcie klasy * .
    • Tak: przeczytaj następne pytanie.
  2. Czy twoja klasa bazowa jest abstrakcyjna? (tj. jakiekolwiek wirtualne czyste metody?)
    • Nie: spróbuj uczynić swoją klasę bazową abstrakcyjną, przeprojektowując hierarchię klas
    • Tak: przeczytaj następne pytanie.
  3. Czy chcesz zezwolić na usuwanie polimorficzne przez wskaźnik podstawowy?
    • Nie: zadeklaruj chroniony wirtualny destruktor, aby zapobiec niepożądanemu użyciu.
    • Tak: zadeklaruj publiczny wirtualny destruktor (w tym przypadku bez narzutu).

Mam nadzieję, że to pomoże.

* Ważne jest, aby pamiętać, że w C ++ nie ma sposobu, aby oznaczyć klasę jako ostateczną (tj. Nie można jej podklasować), więc w przypadku, gdy zdecydujesz się zadeklarować swój destruktor jako niewirtualny i publiczny, pamiętaj, aby wyraźnie ostrzec innych programistów przed wywodzący się z twojej klasy.

Bibliografia:

davidag
źródło
11
Ta odpowiedź jest częściowo nieaktualna, w C ++ jest teraz ostatnie słowo kluczowe.
Étienne
10

Tak, to jest zawsze ważne. Klasy pochodne mogą przydzielać pamięć lub przechowywać odwołania do innych zasobów, które będą musiały zostać wyczyszczone, gdy obiekt zostanie zniszczony. Jeśli nie udostępnisz swoim interfejsom / klasom abstrakcyjnym wirtualnych destruktorów, to za każdym razem, gdy usuniesz instancję klasy pochodnej za pośrednictwem uchwytu klasy bazowej, destruktor klasy pochodnej nie zostanie wywołany.

Dlatego otwierasz potencjał wycieków pamięci

class IFoo
{
  public:
    virtual void DoFoo() = 0;
};

class Bar : public IFoo
{
  char* dooby = NULL;
  public:
    virtual void DoFoo() { dooby = new char[10]; }
    void ~Bar() { delete [] dooby; }
};

IFoo* baz = new Bar();
baz->DoFoo();
delete baz; // memory leak - dooby isn't deleted
Dz.U.
źródło
To prawda, w tym przykładzie może to nie tylko wyciek pamięci, ale prawdopodobnie awaria: - /
Evan Teran,
7

Nie zawsze jest to wymagane, ale uważam, że jest to dobra praktyka. To, co robi, to umożliwia bezpieczne usunięcie obiektu pochodnego za pomocą wskaźnika typu podstawowego.

Na przykład:

Base *p = new Derived;
// use p as you see fit
delete p;

jest źle sformułowany, jeśli Basenie ma wirtualnego destruktora, ponieważ będzie próbował usunąć obiekt tak, jakby był plikiem Base *.

Evan Teran
źródło
nie chcesz naprawiać boost :: shared_pointer p (new Derived), aby wyglądał jak boost :: shared_pointer <Base> p (new Derived); ? może wtedy ppl zrozumieją twoją odpowiedź i zagłosują
Johannes Schaub - litb
EDYCJA: "Skodyfikuj" kilka części, aby nawiasy kątowe były widoczne, jak sugerowano litb.
j_random_hacker,
@EvanTeran: Nie jestem pewien, czy to się zmieniło od czasu pierwotnego opublikowania odpowiedzi (dokumentacja Boost na stronie boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm sugeruje, że tak się stało), ale to nieprawda te dni shared_ptrbędą próbowały usunąć obiekt tak, jakby był Base *- pamięta typ rzeczy, za pomocą której go utworzyłeś. Zobacz odnośnik, do którego się odwołuje, w szczególności bit, który mówi: „Destruktor wywoła funkcję delete z tym samym wskaźnikiem, wraz z oryginalnym typem, nawet jeśli T nie ma wirtualnego destruktora lub jest pusty”.
Stuart Golodetz
@StuartGolodetz: Hmm, możesz mieć rację, ale nie jestem pewien. W tym kontekście może nadal być źle ukształtowany z powodu braku wirtualnego destruktora. Warto przejrzeć.
Evan Teran
@EvanTeran: Na wypadek, gdyby było to pomocne - stackoverflow.com/questions/3899790/shared-ptr-magic .
Stuart Golodetz
5

To nie tylko dobra praktyka. Jest to zasada nr 1 dla dowolnej hierarchii klas.

  1. Najbardziej bazowa klasa hierarchii w C ++ musi mieć wirtualny destruktor

A teraz dlaczego. Weźmy typową hierarchię zwierząt. Wirtualne destruktory przechodzą przez wirtualną wysyłkę, tak jak każde inne wywołanie metody. Weźmy następujący przykład.

Animal* pAnimal = GetAnimal();
delete pAnimal;

Załóżmy, że Animal jest klasą abstrakcyjną. Jedynym sposobem, w jaki C ++ zna właściwy destruktor do wywołania, jest wysłanie metody wirtualnej. Jeśli destruktor nie jest wirtualny, po prostu wywoła destruktor Animal i nie zniszczy żadnych obiektów w klasach pochodnych.

Powodem uczynienia destruktora wirtualnym w klasie bazowej jest to, że po prostu usuwa on wybór z klas pochodnych. Ich destruktor domyślnie staje się wirtualny.

JaredPar
źródło
2
W większości się z tobą zgadzam, ponieważ zazwyczaj definiując hierarchię, chcesz mieć możliwość odniesienia się do obiektu pochodnego za pomocą wskaźnika / odniesienia klasy bazowej. Ale nie zawsze tak jest, aw innych przypadkach może wystarczyć ochrona dtora klasy bazowej.
j_random_hacker,
@j_random_hacker, dzięki czemu jest chroniony, nie ochroni Cię przed nieprawidłowymi wewnętrznymi usunięciem
JaredPar
1
@JaredPar: Zgadza się, ale przynajmniej możesz być odpowiedzialny we własnym kodzie - najtrudniejsze jest upewnienie się, że kod klienta nie spowoduje eksplozji twojego kodu. (Podobnie, uczynienie członka danych prywatnym nie uniemożliwia wewnętrznemu kodowi zrobienia czegoś głupiego z tym członkiem.)
j_random_hacker
@j_random_hacker, przepraszam, że odpowiadam wpisem na blogu, ale naprawdę pasuje do tego scenariusza. blogs.msdn.com/jaredpar/archive/2008/03/24/…
JaredPar
@JaredPar: Świetny post, zgadzam się z tobą w 100%, zwłaszcza jeśli chodzi o sprawdzanie umów w kodzie detalicznym. Chodzi mi tylko o to, że są przypadki, w których wiesz , że nie potrzebujesz wirtualnego dtora. Przykład: klasy tagów do wysłania szablonu. Mają rozmiar 0, dziedziczenie jest używane tylko do wskazania specjalizacji.
j_random_hacker,
3

Odpowiedź jest prosta, potrzebujesz, aby była wirtualna, w przeciwnym razie klasa bazowa nie byłaby pełną klasą polimorficzną.

    Base *ptr = new Derived();
    delete ptr; // Here the call order of destructors: first Derived then Base.

Wolisz powyższe usunięcie, ale jeśli destruktor klasy bazowej nie jest wirtualny, zostanie wywołany tylko destruktor klasy bazowej, a wszystkie dane w klasie pochodnej pozostaną nieusunięte.

fatma.ekici
źródło