Rozumiem potrzebę posiadania wirtualnego destruktora. Ale dlaczego potrzebujemy czystego wirtualnego destruktora? W jednym z artykułów C ++ autor wspomniał, że używamy czystego wirtualnego destruktora, gdy chcemy stworzyć abstrakcję klasy.
Ale możemy uczynić klasę abstrakcyjną, sprawiając, że dowolny element członkowski działa jako czysto wirtualny.
Więc moje pytania są
Kiedy naprawdę sprawiamy, że destruktor jest czysto wirtualny? Czy ktoś może podać dobry przykład w czasie rzeczywistym?
Kiedy tworzymy klasy abstrakcyjne, czy dobrą praktyką jest uczynienie destruktora również czysto wirtualnym? Jeśli tak, to dlaczego?
c++
destructor
pure-virtual
znak
źródło
źródło
Odpowiedzi:
Prawdopodobnie prawdziwym powodem, dla którego dozwolone są czyste wirtualne destruktory, jest to, że zakazanie ich oznaczałoby dodanie kolejnej reguły do języka i nie ma potrzeby stosowania tej reguły, ponieważ zezwolenie na czysty wirtualny destruktor nie może powodować żadnych złych skutków.
Nie, wystarczy zwykły stary wirtualny.
Jeśli tworzysz obiekt z domyślnymi implementacjami jego metod wirtualnych i chcesz uczynić go abstrakcyjnym bez zmuszania kogokolwiek do nadpisania określonego metody, możesz uczynić destruktor czystym wirtualnym. Nie widzę w tym sensu, ale jest to możliwe.
Zauważ, że ponieważ kompilator wygeneruje niejawny destruktor dla klas pochodnych, jeśli autor klasy tego nie zrobi, żadne klasy pochodne nie będą abstrakcyjne. Dlatego posiadanie czystego wirtualnego destruktora w klasie bazowej nie będzie miało żadnego znaczenia dla klas pochodnych. Spowoduje to, że klasa bazowa będzie tylko abstrakcyjna (dzięki za @kappa komentarz ).
Można również założyć, że każda klasa pochodna prawdopodobnie musiałaby mieć określony kod czyszczący i użyć czystego wirtualnego destruktora jako przypomnienia o jego napisaniu, ale wydaje się to wymyślone (i niewymuszone).
Uwaga: Destruktor jest jedyną metodą, która nawet jeśli jest czysto wirtualna, musi mieć implementację w celu utworzenia instancji klas pochodnych (tak, czyste funkcje wirtualne mogą mieć implementacje).
źródło
foof::bar
jeśli chcesz się przekonać.Wszystko, czego potrzebujesz do klasy abstrakcyjnej, to przynajmniej jedna czysta funkcja wirtualna. Każda funkcja się nada; ale tak się składa, że destruktor jest czymś, co będzie miała każda klasa - więc zawsze jest dostępny jako kandydat. Co więcej, uczynienie destruktora czystym wirtualnym (w przeciwieństwie do zwykłego wirtualnego) nie ma żadnych behawioralnych skutków ubocznych poza uczynieniem klasy abstrakcyjną. W związku z tym wiele przewodników po stylach zaleca konsekwentne stosowanie czystego wirtualnego elementu destuctor, aby wskazać, że klasa jest abstrakcyjna - choćby z innego powodu niż zapewnia spójne miejsce, w którym ktoś czytający kod może sprawdzić, czy klasa jest abstrakcyjna.
źródło
Jeśli chcesz utworzyć abstrakcyjną klasę bazową:
... najłatwiej jest uczynić klasę abstrakcyjną, czyniąc destruktor czystym wirtualnym i dostarczając dla niego definicję (treść metody).
Dla naszego hipotetycznego abecadła:
Gwarantujesz, że nie można go utworzyć instancji (nawet wewnątrz samej klasy, dlatego prywatne konstruktory mogą nie wystarczyć), otrzymujesz wirtualne zachowanie, które chcesz dla destruktora, i nie musisz znajdować i oznaczać innej metody, która tego nie robi Wirtualna wysyłka nie jest potrzebna jako „wirtualna”.
źródło
Z odpowiedzi, które przeczytałem na twoje pytanie, nie mogłem wydedukować dobrego powodu, aby faktycznie użyć czystego wirtualnego destruktora. Na przykład następujący powód w ogóle mnie nie przekonuje:
Moim zdaniem przydatne mogą być czyste wirtualne destruktory. Na przykład załóżmy, że masz w kodzie dwie klasy myClassA i myClassB i że myClassB dziedziczy po myClassA. Z powodów wymienionych przez Scotta Meyersa w jego książce „Bardziej efektywny C ++”, punkt 33 „Tworzenie abstrakcyjnych klas innych niż liście”, lepszą praktyką jest tworzenie klasy abstrakcyjnej myAbstractClass, z której dziedziczą myClassA i myClassB. Zapewnia to lepszą abstrakcję i zapobiega niektórym problemom wynikającym na przykład z kopiowaniem obiektów.
W procesie abstrakcji (tworzenia klasy myAbstractClass) może się zdarzyć, że żadna metoda myClassA lub myClassB nie będzie dobrym kandydatem do bycia czystą metodą wirtualną (co jest warunkiem abstrakcji myAbstractClass). W tym przypadku definiujesz destruktor klasy abstrakcyjnej jako czysty wirtualny.
Poniżej konkretny przykład z kodu, który sam napisałem. Mam dwie klasy, Numerics / PhysicsParams, które mają wspólne właściwości. Dlatego pozwoliłem im dziedziczyć po abstrakcyjnej klasie IParams. W tym przypadku nie miałem pod ręką żadnej metody, która mogłaby być czysto wirtualna. Na przykład metoda setParameter musi mieć tę samą treść dla każdej podklasy. Jedynym wyborem, jaki miałem, było uczynienie destruktora IParams czystym wirtualnym.
źródło
IParam
ma być chroniony, jak zauważono w innym komentarzu.Jeśli chcesz zatrzymać tworzenie instancji klasy bazowej bez dokonywania jakichkolwiek zmian w już zaimplementowanej i przetestowanej klasie pochodnej, zaimplementujesz czysty wirtualny destruktor w swojej klasie bazowej.
źródło
Tutaj chcę powiedzieć, kiedy potrzebujemy wirtualnego destruktora, a kiedy czystego wirtualnego destruktora
Jeśli chcesz, aby nikt nie mógł bezpośrednio utworzyć obiektu klasy Base, użyj czystego wirtualnego destruktora
virtual ~Base() = 0
. Zwykle wymagana jest przynajmniej jedna czysta funkcja wirtualna, przyjmijmyvirtual ~Base() = 0
, że jest to funkcja.Kiedy nie potrzebujesz powyższej rzeczy, potrzebujesz tylko bezpiecznego zniszczenia obiektu klasy pochodnej
Baza * pBase = new Derived (); usuń pBase; czysty wirtualny destruktor nie jest wymagany, tylko wirtualny destruktor wykona zadanie.
źródło
Z tymi odpowiedziami wchodzisz w hipotetyczne, więc dla jasności spróbuję przedstawić prostsze, bardziej przyziemne wyjaśnienie.
Podstawowe zależności w projektowaniu obiektowym są dwie: IS-A i HAS-A. Ja tego nie wymyśliłem. Tak się nazywają.
IS-A wskazuje, że określony obiekt identyfikuje się jako należący do klasy znajdującej się nad nim w hierarchii klas. Obiekt banana jest obiektem owocu, jeśli jest podklasą klasy owoców. Oznacza to, że wszędzie tam, gdzie można użyć klasy owoców, można użyć banana. Nie jest to jednak odruchowe. Nie można podstawić klasy bazowej dla określonej klasy, jeśli ta konkretna klasa jest wywoływana.
Has-a wskazał, że obiekt jest częścią klasy złożonej i że istnieje relacja własności. W C ++ oznacza to, że jest to obiekt składowy i jako taki na klasie będącej właścicielem spoczywa obowiązek pozbycia się go lub przekazania własności przed zniszczeniem samego siebie.
Te dwie koncepcje są łatwiejsze do zrealizowania w językach z pojedynczym dziedziczeniem niż w modelu wielokrotnego dziedziczenia, takim jak c ++, ale zasady 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 pobiera wskaźnik klasy Fruit.
Funkcje wirtualne są, po pierwsze, kwestią czasu wykonywania. Jest częścią polimorfizmu, ponieważ służy do decydowania, która funkcja ma być uruchomiona w momencie wywołania w uruchomionym programie.
Słowo kluczowe virtual jest dyrektywą kompilatora, która wiąże funkcje w określonej kolejności, jeśli istnieje niejasność dotycząca tożsamości klasy. Funkcje wirtualne są zawsze w klasach nadrzędnych (o ile wiem) i wskazują kompilatorowi, że powiązanie funkcji składowych z ich nazwami powinno odbywać się najpierw z funkcją podklasy, a następnie z funkcją klasy nadrzędnej.
Klasa Fruit może mieć wirtualną funkcję color (), która domyślnie zwraca „NONE”. Funkcja Color () klasy Banana zwraca „ŻÓŁTY” lub „BRĄZOWY”.
Ale jeśli funkcja pobierająca wskaźnik Fruit wywoła color () w wysłanej do niej klasie Banana - która funkcja color () zostanie wywołana? Funkcja normalnie wywołałaby Fruit :: color () dla obiektu Fruit.
W 99% przypadków nie byłoby to zamierzone. Ale jeśli Fruit :: color () zostałaby zadeklarowana jako wirtualna, wówczas Banana: color () zostałby wywołany dla obiektu, ponieważ prawidłowa funkcja color () byłaby powiązana ze wskaźnikiem Fruit w momencie wywołania. Środowisko uruchomieniowe sprawdzi, na który obiekt wskazuje wskaźnik, ponieważ został on oznaczony jako wirtualny w definicji klasy Fruit.
Różni się to od zastępowania funkcji w podklasie. W takim przypadku wskaźnik Fruit wywoła Fruit :: color (), jeśli jedyne, co wie, to to, że jest wskaźnikiem IS-A 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 wywoływana. Rzeczywiście, nie można wywołać czystej funkcji wirtualnej. Jednak nadal należy go zdefiniować. Podpis funkcji musi istnieć. Wielu programistó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 wskazuje na Fruit, zostanie wywołana Banana :: color (), ponieważ jest to jedyna dostępna implementacja color ().
Teraz ostatni element układanki: konstruktorzy i destruktory.
Czyste wirtualne konstruktory są całkowicie nielegalne. To właśnie się skończyło.
Ale czyste wirtualne destruktory działają w przypadku, gdy chcesz zabronić tworzenia instancji klasy bazowej. Tylko podklasy mogą być tworzone, jeśli destruktor klasy bazowej jest czysto wirtualny. Konwencja polega na przypisaniu go do 0.
W takim przypadku musisz utworzyć implementację. Kompilator wie, że to właśnie robisz i upewnia się, że robisz to dobrze, lub bardzo narzeka, że nie może połączyć się ze wszystkimi funkcjami, których potrzebuje do skompilowania. Błędy mogą być mylące, jeśli nie jesteś na dobrej drodze, jeśli chodzi o modelowanie hierarchii klas.
Więc w tym przypadku nie możesz tworzyć instancji Fruit, ale możesz tworzyć instancje Banana.
Wywołanie usunięcia wskaźnika Fruit, który wskazuje na instancję klasy Banana, najpierw wywoła Banana :: ~ Banana (), a następnie zawsze Fuit :: ~ Fruit (). Ponieważ bez względu na wszystko, kiedy wywołujesz destruktor podklasy, musi być zgodny z destruktorem klasy bazowej.
Czy to zły model? Jest to bardziej skomplikowane na etapie projektowania, tak, ale może zapewnić prawidłowe łączenie w czasie wykonywania oraz wykonywanie funkcji podklasy w przypadku niejasności co do tego, do której podklasy uzyskuje się dostęp.
Jeśli piszesz w C ++ tak, że przekazujesz tylko dokładne wskaźniki do klas bez wskaźników ogólnych ani niejednoznacznych, wtedy funkcje wirtualne nie są tak naprawdę potrzebne. Ale jeśli potrzebujesz elastyczności typów w czasie wykonywania (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 owocu i wiesz, że każdy owoc zareaguje na color () swoją własną, poprawną funkcją.
Mam nadzieję, że to rozwlekłe wyjaśnienie raczej utrwali koncepcję, a nie wprowadzi w błąd. Istnieje wiele dobrych przykładów, na które można spojrzeć, przyjrzeć się im wystarczająco dużo, uruchomić je i zadzierać z nimi, a otrzymasz to.
źródło
To temat sprzed dziesięciu lat :) Przeczytaj ostatnie 5 akapitów punktu 7 w książce „Efektywny C ++”, aby uzyskać szczegółowe informacje, zaczyna się od „Czasami wygodnie jest nadać klasie czysty wirtualny destruktor…”
źródło
Poprosiłeś o przykład i uważam, że poniższe uzasadnienie dla czystego wirtualnego destruktora. Z niecierpliwością czekam na odpowiedzi, czy to dobry powód ...
Nie chcę, aby ktokolwiek mógł wrzucić
error_base
typ, ale typy wyjątkówerror_oh_shucks
ierror_oh_blast
mają identyczną funkcjonalność i nie chcę pisać tego dwa razy. Złożoność pImpl jest konieczna, aby uniknąć ujawnieniastd::string
moim klientom i użyciastd::auto_ptr
wymaga konstruktora kopiującego.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 rzucanych przez moją bibliotekę:
A oto wspólna implementacja:
Klasa wyjątku_string, utrzymywana jako prywatna, ukrywa std :: string z mojego publicznego interfejsu:
Mój kod następnie zgłasza błąd jako:
Korzystanie z szablonu
error
jest trochę nieodpłatne. Oszczędza trochę kodu kosztem wymagania od klientów wychwytywania błędów, takich jak:źródło
Może jest inny PRAWDZIWY PRZYPADEK czystego wirtualnego destruktora, którego właściwie nie widzę w innych odpowiedziach :)
Na początku całkowicie zgadzam się z zaznaczoną odpowiedzią: To dlatego, że zakazanie czystego wirtualnego destruktora wymagałoby dodatkowej reguły w specyfikacji języka. Ale nadal nie jest to przypadek użycia, do którego wzywa Mark :)
Najpierw wyobraź sobie to:
i coś takiego:
Po prostu - mamy interfejs
Printable
i jakiś „kontener” przechowujący wszystko z tym interfejsem. Myślę, że tutaj jest całkiem jasne, dlaczegoprint()
metoda jest czysto wirtualna. Może mieć jakąś treść, ale jeśli nie ma domyślnej implementacji, czysta wirtualność jest idealną "implementacją" (= "musi być dostarczone przez klasę potomną").A teraz wyobraź sobie dokładnie to samo, z wyjątkiem tego, że nie jest to drukowanie, ale zniszczenie:
A także może być podobny pojemnik:
To uproszczony przypadek użycia z mojej prawdziwej aplikacji. Jedyną różnicą jest to, że zamiast metody „normalnej” użyto metody „specjalnej” (destructor)
print()
. Ale powód, dla którego jest to czysto wirtualny, jest nadal ten sam - nie ma domyślnego kodu dla metody. Nieco zagmatwany może być fakt, że MUSI istnieć efektywnie jakiś destruktor, a kompilator faktycznie generuje dla niego pusty kod. Ale z punktu widzenia programisty czysta wirtualność wciąż oznacza: „Nie mam żadnego domyślnego kodu, musi być dostarczony przez klasy pochodne”.Myślę, że to nie jest żaden wielki pomysł, tylko więcej wyjaśnienia, że czysta wirtualność działa naprawdę jednolicie - również w przypadku destruktorów.
źródło
1) Gdy chcesz, aby klasy pochodne były czyszczone. To jest rzadkie.
2) Nie, ale chcesz, aby był wirtualny.
źródło
musimy uczynić destruktor wirtualnym, ponieważ jeśli nie uczynimy go wirtualnym, to kompilator zniszczy tylko zawartość klasy bazowej, n wszystkie klasy pochodne pozostaną niezmienione, kompilator bacuse nie będzie wywoływał destruktora żadnego innego klasa z wyjątkiem klasy bazowej.
źródło
delete
wskaźnik do klasy bazowej, podczas gdy w rzeczywistości wskazuje on na jego pochodną.