Po co nam czysty wirtualny Destruktor w C++?

Rozumiem potrzebę Wirtualnego destruktora. Ale po co nam czysty wirtualny Destruktor? W jednym z artykułów C++ autor wspomniał, że używamy pure virtual destructor, gdy chcemy, aby klasa była abstrakcyjna.

Ale możemy uczynić klasę abstrakcyjną, czyniąc dowolną z funkcji Członkowskich czystą wirtualną.

Więc moje pytania to

  1. Kiedy naprawdę uczynimy Destruktor czystym wirtualnym? Czy ktoś może podać dobry przykład czasu rzeczywistego?

  2. Kiedy tworzymy klasy abstrakcyjne, czy dobrą praktyką jest uczynienie destruktora czystym wirtualnym? Jeśli tak..więc dlaczego?

Author: Motti, 2009-08-02

12 answers

  1. Prawdopodobnie prawdziwym powodem, dla którego dozwolone są czyste wirtualne destruktory, jest to, że zakazanie ich oznaczałoby dodanie innej reguły do języka i nie ma takiej potrzeby, ponieważ żadne złe skutki nie mogą wynikać z zezwolenia na czysty wirtualny Destruktor.

  2. Nie, wystarczy zwykły stary wirtualny.

Jeśli tworzysz obiekt z domyślnymi implementacjami dla jego metod wirtualnych i chcesz, aby był abstrakcyjny bez zmuszania kogokolwiek do nadpisywania żadnych specyficzna metoda, możesz uczynić Destruktor czystym wirtualnym. Nie widzę w tym sensu, ale to możliwe.

Zauważ, że ponieważ kompilator wygeneruje niejawny destruktor dla klas pochodnych, jeśli autor klasy tego nie zrobi, wszelkie klasy pochodne będą , a nie abstrakcyjne. Dlatego posiadanie czystego Wirtualnego destruktora w klasie bazowej nie będzie miało żadnej różnicy dla klas pochodnych. To tylko uczyni klasę bazową abstrakcyjną (dzięki za @ kappa 's komentarz).

Można również założyć, że każda klasa pochodna prawdopodobnie musiałaby mieć specyficzny kod do czyszczenia i używać czystego destruktora Wirtualnego jako przypomnienia, aby go napisać, ale wydaje się to wymyślone (i nie wymuszone).

Uwaga: Destruktor jest jedyną metodą, która nawet jeśli jestpure virtual ma mieć implementację w celu tworzenia instancji klas pochodnych (tak czyste funkcje wirtualne mogą mieć wdrożenia).

struct foo {
    virtual void bar() = 0;
};

void foo::bar() { /* default implementation */ }

class foof : public foo {
    void bar() { foo::bar(); } // have to explicitly call default implementation.
};
 104
Author: Motti,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:20

Wszystko czego potrzebujesz do klasy abstrakcyjnej to co najmniej jedna czysta funkcja wirtualna. Każda funkcja będzie działać; ale tak się składa, że Destruktor jest czymś, co każda klasa będzie miała-więc zawsze jest tam jako kandydat. Co więcej, uczynienie destruktora czystym wirtualnym (w przeciwieństwie do tylko Wirtualnego) nie ma żadnych behawioralnych skutków ubocznych, innych niż uczynienie klasy abstrakcyjną. W związku z tym wiele przewodników po stylach zaleca konsekwentne używanie czystego Wirtualnego destuktora, aby wskazać, że klasa jest abstract-jeśli z żadnego innego powodu niż zapewnia spójne miejsce, ktoś czytający kod może sprawdzić, czy klasa jest abstrakcyjna.

 28
Author: Braden,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-02 22:03:20

Jeśli chcesz utworzyć abstrakcyjną klasę bazową:

  • that can ' t be instantiated (tak, to jest zbędne z terminem "abstrakcyjny"!)
  • ale wymaga zachowania Wirtualnego destruktora (zamierzasz przenosić wskaźniki do ABC zamiast wskaźników do typów pochodnych i usuwać przez nie)
  • ale nie wymaga żadnego innego wirtualnego zachowania dla innych metod (może nie ma żadnych innych metod? rozważ prosty protected" resource " container that needs a constructor/destructor / assignment but not much more)

...najłatwiej jest uczynić klasę abstrakcyjną, czyniąc Destruktor czystym wirtualnym i podając dla niej definicję (ciało metody).

Dla naszego hipotetycznego ABC:

Gwarantujesz, że nie może być utworzona (nawet wewnętrzna do samej klasy, dlatego prywatne konstruktory mogą nie wystarczyć), otrzymujesz wirtualne zachowanie, które chcesz dla destruktora, i nie musisz znaleźć i oznaczyć innej metody, która nie wymaga wirtualnej wysyłki jako "wirtualna".

 18
Author: leander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-02 20:14:25

Z odpowiedzi, które przeczytałem na twoje pytanie, nie mogłem wydedukować dobrego powodu, aby użyć czystego Wirtualnego destruktora. Na przykład, następujący powód w ogóle mnie nie przekonuje:

Prawdopodobnie prawdziwym powodem, dla którego dozwolone są czyste wirtualne destruktory, jest to, że zakazanie ich oznaczałoby dodanie innej reguły do języka i nie ma takiej potrzeby, ponieważ Zezwolenie na czysty wirtualny Destruktor nie powoduje żadnych złych skutków.

Moim zdaniem czysty wirtualny destruktory mogą się przydać. Załóżmy na przykład, że w kodzie masz dwie klasy myClassA i myClassB, a myClassB dziedziczy po myClassA. Z powodów wymienionych przez Scotta Meyersa w jego książce "More Effective C++", poz. 33 "Making non-leaf classes abstract", lepszą praktyką jest stworzenie abstrakcyjnej klasy myAbstractClass, z której dziedziczą myClassA i myClassB. Zapewnia to lepszą abstrakcję i zapobiega powstawaniu pewnych problemów, na przykład z kopiami obiektów.

W proces abstrakcji (tworzenia klasy myAbstractClass), może być tak, że żadna metoda myClassA lub myClassB nie jest dobrym kandydatem do bycia czystą metodą wirtualną (co jest warunkiem wstępnym, aby myabstractclass był abstrakcyjny). W tym przypadku definiujesz Destruktor klasy abstrakcyjnej pure virtual.

Poniżej konkretny przykład z jakiegoś kodu, który sam napisałem. Mam dwie klasy, Numeryczne / Fizyczneparamy, które mają wspólne właściwości. Dlatego pozwalam im dziedziczyć z abstrakcji Klasa IParams. W tym przypadku nie miałem absolutnie żadnej metody, która mogłaby być czysto wirtualna. Na przykład metoda setParameter musi mieć takie samo ciało dla każdej podklasy. Jedynym wyborem, jaki miałem, było uczynienie destruktora IParams czystym wirtualnym.
struct IParams
{
    IParams(const ModelConfiguration& aModelConf);
    virtual ~IParams() = 0;

    void setParameter(const N_Configuration::Parameter& aParam);

    std::map<std::string, std::string> m_Parameters;
};

struct NumericsParams : IParams
{
    NumericsParams(const ModelConfiguration& aNumericsConf);
    virtual ~NumericsParams();

    double dt() const;
    double ti() const;
    double tf() const;
};

struct PhysicsParams : IParams
{
    PhysicsParams(const N_Configuration::ModelConfiguration& aPhysicsConf);
    virtual ~PhysicsParams();

    double g()     const; 
    double rho_i() const; 
    double rho_w() const; 
};
 7
Author: Laurent Michel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-11 14:56:28

Jeśli chcesz przestać tworzyć instancje klasy bazowej bez wprowadzania jakichkolwiek zmian w już zaimplementowanej i przetestowanej klasie derive, zaimplementujesz czysty wirtualny Destruktor w swojej klasie bazowej.

 3
Author: sukumar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-21 09:20:20

Tutaj chcę powiedzieć, Kiedy potrzebujemy virtual destructor i kiedy potrzebujemy pure virtual destructor

class Base
{
public:
    Base();
    virtual ~Base() = 0; // Pure virtual, now no one can create the Base Object directly 
};

Base::Base() { cout << "Base Constructor" << endl; }
Base::~Base() { cout << "Base Destructor" << endl; }


class Derived : public Base
{
public:
    Derived();
    ~Derived();
};

Derived::Derived() { cout << "Derived Constructor" << endl; }
Derived::~Derived() {   cout << "Derived Destructor" << endl; }


int _tmain(int argc, _TCHAR* argv[])
{
    Base* pBase = new Derived();
    delete pBase;

    Base* pBase2 = new Base(); // Error 1   error C2259: 'Base' : cannot instantiate abstract class
}
  1. Jeśli chcesz, aby nikt nie mógł bezpośrednio utworzyć obiektu klasy bazowej, użyj pure virtual destructor virtual ~Base() = 0. Zwykle wymagana jest co najmniej jedna czysta funkcja wirtualna, weźmy virtual ~Base() = 0, jako tę funkcję.

  2. Kiedy nie potrzebujesz powyższej rzeczy, potrzebujesz tylko bezpiecznego zniszczenia pochodnego obiektu klasy

    Baza* pBase = New Derived(); usunąć; czysty wirtualny destructor nie jest wymagany, tylko wirtualny destructor wykona to zadanie.

 1
Author: Anil8753,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-10 09:46:17

[1]}wchodzisz w hipotezy z tymi odpowiedziami, więc postaram się zrobić prostsze, bardziej przyziemne Wyjaśnienie dla jasności.

Podstawowe relacje projektowania zorientowanego obiektowo to dwa: Nie zmyśliłem tego. Tak się je nazywa.

IS-a oznacza, że dany obiekt identyfikuje się jako należący do klasy, która znajduje się powyżej niego w hierarchii klas. Obiekt bananowy jest obiektem owocowym, jeśli jest podklasą klasy owocowej. Oznacza to że wszędzie, gdzie można użyć klasy owoców, można użyć banana. Nie jest to jednak refleksyjne. Nie można zastąpić klasy bazowej konkretną klasą, jeśli ta konkretna klasa jest wywoływana.

Has-a wskazuje, że obiekt jest częścią klasy złożonej i że istnieje relacja własności. Oznacza to w C++, że jest to obiekt członkowski i jako taki klasa jest zobowiązana do pozbycia się go lub przekazania własności przed zniszczeniem.

Te dwa pojęcia to łatwiejsze do zrealizowania w językach dziedziczenia pojedynczego niż w modelu dziedziczenia wielokrotnego, takim jak c++, ale reguły są zasadniczo takie same. Komplikacja pojawia się, gdy tożsamość klasy jest niejednoznaczna, na przykład przekazanie wskaźnika klasy Banana do funkcji, która przyjmuje wskaźnik klasy Fruit.

Funkcje wirtualne są, po pierwsze, rzeczą wykonywalną. Jest to część polimorfizmu, ponieważ służy do decydowania, którą funkcję uruchomić w czasie, gdy jest wywoływana w uruchomionym programie.

The virtual keyword jest dyrektywą kompilatora do wiązania funkcji w określonej kolejności, jeśli istnieje niejednoznaczność co do tożsamości klasy. Funkcje wirtualne są zawsze w klasach nadrzędnych (o ile mi wiadomo) i wskazują kompilatorowi, że powiązanie funkcji Członkowskich z ich nazwami powinno mieć miejsce najpierw z funkcją podklasy, a później z funkcją klasy nadrzędnej.

Klasa Fruit może mieć wirtualną funkcję color (), która domyślnie zwraca "NONE". Funkcja Banana class color() zwraca "Żółty" lub "brązowy".

Ale jeśli funkcja biorąca wskaźnik owoców wywoła color () NA wysłanej do niej klasie Banana - która funkcja color () zostanie wywołana? Funkcja normalnie wywoła metodę Fruit:: color () Dla obiektu Fruit.

To by w 99% nie było to, co było zamierzone. Ale jeśli Fruit:: color () została zadeklarowana jako wirtualna, to Banana: color () zostanie wywołana dla obiektu, ponieważ prawidłowa funkcja color () byłaby powiązana ze wskaźnikiem Fruit w czasie wywołania. The runtime sprawdzi, do jakiego obiektu wskazuje wskaźnik, ponieważ został on oznaczony jako wirtualny w definicji klasy Fruit.

To coś innego niż nadpisanie funkcji w podklasie. W takim razie wskaźnik Fruit wywoła metodę Fruit:: color (), jeśli wie tylko, że jest-wskaźnik do Fruit.

Więc teraz pojawia się idea "czystej funkcji wirtualnej". Jest to raczej niefortunne zdanie, ponieważ czystość nie ma z tym nic wspólnego. Oznacza to, że jest zamierzone, aby metoda klasy bazowej nigdy nie była dzwoniłem. Rzeczywiście czysta funkcja wirtualna nie może być wywołana. Musi jednak zostać jeszcze zdefiniowana. Musi istnieć podpis funkcji. Wiele koderów tworzy pustą implementację {} dla kompletności, ale kompilator wygeneruje ją wewnętrznie, jeśli nie. W takim przypadku , gdy funkcja jest wywoływana nawet jeśli wskaźnik jest do Fruit, Banana:: color() zostanie wywołana, ponieważ jest to jedyna implementacja color ().

Teraz ostatni element układanki: konstruktory i destruktory.

Pure wirtualne konstruktory są całkowicie nielegalne. To jest po prostu Na Zewnątrz.

Ale czyste destruktory wirtualne działają w przypadku, gdy chcesz zabronić tworzenia instancji klasy bazowej. Tylko podklasy mogą być utworzone, jeśli Destruktor klasy bazowej jest czysty wirtualny. konwencją jest przypisanie jej do 0.

 virtual ~Fruit() = 0;  // pure virtual 
 Fruit::~Fruit(){}      // destructor implementation

W tym przypadku musisz utworzyć implementację. Kompilator wie, że to jest to, co robisz i upewnia się, że robisz to dobrze, lub narzeka potężnie, że to nie można połączyć się ze wszystkimi funkcjami potrzebnymi do kompilacji. Błędy mogą być mylące, jeśli nie jesteś na dobrej drodze do modelowania swojej hierarchii klasowej.

Więc w tym przypadku nie wolno Ci tworzyć instancji owoców, ale wolno Ci tworzyć instancji bananów.

Wezwanie do usunięcia wskaźnika owoców, który wskazuje na instancję Banana najpierw wywoła Banana:: ~ Banana (), a następnie fuit::~Fruit(), zawsze. Bo bez względu na wszystko, kiedy wywołasz podklasę Destruktor, Destruktor klasy podstawowej musi podążać za nim.

Czy to zły model? Jest to bardziej skomplikowane w fazie projektowania, tak, ale może zapewnić, że poprawne łączenie jest wykonywane w czasie wykonywania i że funkcja podklasy jest wykonywana tam, gdzie istnieje niejasność, która dokładnie podklasa jest dostępna.

Jeśli piszesz C++ tak, że przekazujesz tylko dokładne wskaźniki klas bez ogólnych lub niejednoznacznych wskaźników, to funkcje wirtualne nie są tak naprawdę potrzebne. Ale jeśli potrzebujesz run-time elastyczność typów (jak w Apple Banana Orange ==> Fruit ) funkcje stają się łatwiejsze i bardziej wszechstronne z mniej redundantnym kodem. Nie musisz już pisać funkcji dla każdego rodzaju owoców i wiesz, że każdy owoc odpowie na color () z własną poprawną funkcją.

Mam nadzieję, że to długofalowe Wyjaśnienie umocni tę koncepcję, a nie pomieszać rzeczy. Istnieje wiele dobrych przykładów, aby spojrzeć na, i patrzeć na wystarczająco i faktycznie uruchomić je i bałagan z oni i dostaniesz to.

 1
Author: Chris Reid,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-11 07:46:04

Poprosiłeś o Przykład, i wierzę, że poniżej znajduje się powód dla czystego Wirtualnego destruktora. Czekam na odpowiedzi, czy jest to dobry powód...

Nie chcę, aby ktokolwiek mógł rzucić typ error_base, ale typy WYJĄTKÓW error_oh_shucks i error_oh_blast mają identyczną funkcjonalność i nie chcę pisać tego dwa razy. Pimpleks jest niezbędny, aby nie wystawiać std::string moim klientom, a użycie std::auto_ptr wymaga kopii konstruktor.

Nagłówek publiczny zawiera specyfikacje wyjątków, które będą dostępne dla Klienta w celu rozróżnienia różnych typów wyjątków wyrzucanych przez moją bibliotekę:

// error.h

#include <exception>
#include <memory>

class exception_string;

class error_base : public std::exception {
 public:
  error_base(const char* error_message);
  error_base(const error_base& other);
  virtual ~error_base() = 0; // Not directly usable

  virtual const char* what() const;
 private:
  std::auto_ptr<exception_string> error_message_;
};

template<class error_type>
class error : public error_base {
 public:
   error(const char* error_message) : error_base(error_message) {}
   error(const error& other) : error_base(other) {}
   ~error() {}
};

// Neither should these classes be usable
class error_oh_shucks { virtual ~error_oh_shucks() = 0; }
class error_oh_blast { virtual ~error_oh_blast() = 0; }

A oto wspólna implementacja:

// error.cpp

#include "error.h"
#include "exception_string.h"

error_base::error_base(const char* error_message)
  : error_message_(new exception_string(error_message)) {}

error_base::error_base(const error_base& other)
  : error_message_(new exception_string(other.error_message_->get())) {}

error_base::~error_base() {}

const char* error_base::what() const {
  return error_message_->get();
}

Klasa exception_string, utrzymywana jako prywatna, ukrywa std:: string z mojego publicznego interfejsu:

// exception_string.h

#include <string>

class exception_string {
 public:
  exception_string(const char* message) : message_(message) {}

  const char* get() const { return message_.c_str(); }
 private:
  std::string message_;
};

Mój kod wyrzuca błąd jako:

#include "error.h"

throw error<error_oh_shucks>("That didn't work");

Użycie szablonu dla error jest trochę bezinteresowne. Oszczędza bit kodu kosztem wymagania od klientów wyłapywania błędów jako:

// client.cpp

#include <error.h>

try {
} catch (const error<error_oh_shucks>&) {
} catch (const error<error_oh_blast>&) {
}
 0
Author: Rai,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-16 16:58:40

Może jest inny prawdziwy przypadek użycia czystego Wirtualnego destruktora, którego nie widzę w innych odpowiedziach:)

Na początku całkowicie zgadzam się z zaznaczoną odpowiedzią: dzieje się tak dlatego, że zakazanie czystego Wirtualnego destruktora wymagałoby dodatkowej reguły w specyfikacji języka. Ale to wciąż nie jest przypadek użycia, do którego wzywa Mark:)

Najpierw wyobraź sobie to:

class Printable {
  virtual void print() const = 0;
  // virtual destructor should be here, but not to confuse with another problem
};

I coś w stylu:

class Printer {
  void queDocument(unique_ptr<Printable> doc);
  void printAll();
};

Po prostu-mamy interfejs Printable i jakiś " kontener" trzymając cokolwiek z tym interfejsem. Myślę, że tutaj jest całkiem jasne, dlaczego print() metoda jest czysta wirtualna. Może mieć pewne ciało, ale w przypadku, gdy nie ma domyślnej implementacji, pure virtual jest idealną "implementacją" (="musi być dostarczona przez klasę potomną").

A teraz wyobraź sobie dokładnie to samo, z tym, że nie jest to do druku, ale do zniszczenia:

class Destroyable {
  virtual ~Destroyable() = 0;
};

A także może być podobny pojemnik:

class PostponedDestructor {
  // Queues an object to be destroyed later.
  void queObjectForDestruction(unique_ptr<Destroyable> obj);
  // Destroys all already queued objects.
  void destroyAll();
};

To uproszczony przypadek użycia z mojej prawdziwej aplikacji. Na jedyną różnicą jest to, że użyto metody "specjalnej" (Destruktor) zamiast "normalnej" print(). Ale powód, dla którego jest to czysta wirtualna, jest nadal taki sam - nie ma domyślnego kodu dla metody. Nieco mylący może być fakt, że musi istnieć jakiś Destruktor, a kompilator generuje dla niego pusty kod. Ale z punktu widzenia programisty czysta wirtualność nadal oznacza: "nie mam żadnego domyślnego kodu, musi być dostarczany przez klasy pochodne."

Myślę, że to nie ma tu żadnego wielkiego pomysłu, tylko wyjaśnienie, że czysta wirtualność działa naprawdę jednolicie - także dla destruktorów.

 0
Author: Jarek C,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-30 09:40:39

To temat sprzed dekady :) Przeczytaj ostatnie 5 paragrafów punktu # 7 na "Effective C++" książki dla szczegółów, zaczyna się od "czasami może być wygodne, aby dać klasie czysty wirtualny Destruktor...."

 0
Author: J-Q,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-02 08:28:39

1) gdy chcesz wymagać, aby klasy pochodne wykonały czyszczenie. To rzadkość.

2) nie, ale chcesz, żeby to było wirtualne.

 -2
Author: Steven Sudit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-02 19:29:55

Musimy uczynić Destruktor wirtualnym ze względu na fakt , że jeśli nie uczynimy destruktora wirtualnym , kompilator zniszczy tylko zawartość klasy bazowej , n wszystkie pochodne klasy pozostaną niezmienione, kompilator bacuse nie wywoła destruktora żadnej innej klasy poza klasą bazową.

 -2
Author: Asad hashmi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-09 19:00:15