Wierzyłem, że wielokrotnie szukałem wirtualnych destruktorów, większość wspomina o celu wirtualnych destruktorów i dlaczego potrzebujesz wirtualnych destruktorów. Myślę też, że w większości przypadków destruktory muszą być wirtualne.
Zatem pytanie brzmi: dlaczego c ++ domyślnie nie ustawia wirtualnych wszystkich destruktorów? lub w innych pytaniach:
Kiedy NIE muszę używać wirtualnych niszczycieli?
W takim przypadku NIE powinienem używać wirtualnych niszczycieli?
Jaki jest koszt korzystania z wirtualnych niszczycieli, jeśli go używam, nawet jeśli nie jest potrzebny?
c++
virtual-functions
grgrr
źródło
źródło
Odpowiedzi:
Jeśli dodasz wirtualny destruktor do klasy:
w większości (wszystkich?) bieżących implementacji C ++ każda instancja obiektu tej klasy musi przechowywać wskaźnik do wirtualnej tabeli wysyłania dla typu środowiska wykonawczego, a sama wirtualna tabela wysyłania jest dodawana do obrazu wykonywalnego
adres wirtualnej tabeli wysyłania niekoniecznie jest prawidłowy dla różnych procesów, co może uniemożliwić bezpieczne współdzielenie takich obiektów w pamięci współdzielonej
osadzony wirtualny wskaźnik frustruje tworzenie klasy z układem pamięci pasującym do znanego formatu wejściowego lub wyjściowego (na przykład, więc
Price_Tick*
można skierować bezpośrednio na odpowiednio wyrównaną pamięć w przychodzącym pakiecie UDP i użyć do parsowania / dostępu lub zmiany danych, lub umieszczanienew
takiej klasy w celu zapisywania danych w pakiecie wychodzącym)same wywołania destruktora mogą - pod pewnymi warunkami - być wysyłane wirtualnie, a zatem poza linią, podczas gdy nie-wirtualne niszczyciele mogą być nachylone lub zoptymalizowane, jeśli są trywialne lub nieistotne dla dzwoniącego
Argument „nieprzeznaczony do dziedziczenia” nie byłby praktycznym powodem, dla którego nie zawsze miałby się wirtualny destruktor, gdyby nie było gorzej w praktyczny sposób, jak wyjaśniono powyżej; ale biorąc pod uwagę, że jest gorzej, jest to główne kryterium określające, kiedy należy ponieść koszty: domyślnie mieć wirtualny destruktor, jeśli twoja klasa ma być używana jako klasa podstawowa . Nie zawsze jest to konieczne, ale zapewnia, że klasy w hierarchii mogą być używane bardziej swobodnie, bez przypadkowego niezdefiniowanego zachowania, jeśli wywoływany destruktor klasy zostanie wywołany za pomocą wskaźnika klasy podstawowej lub odwołania.
Nie tak ... wiele klas nie ma takiej potrzeby. Jest tak wiele przykładów, w których niepotrzebne jest ich wyliczenie, ale po prostu przejrzyj swoją Standardową Bibliotekę lub powiedz „boost”, a zobaczysz, że istnieje znaczna większość klas, które nie mają wirtualnych destruktorów. W doładowaniu 1,53 liczę 72 wirtualnych destrukcyjnych z 494.
źródło
BTW,
Dla klas podstawowych z usuwaniem polimorficznym.
źródło
Koszt wprowadzenia dowolnej funkcji wirtualnej do klasy (odziedziczonej lub części definicji klasy) jest prawdopodobnie bardzo stromy (lub nie zależy od obiektu) początkowy koszt wirtualnego wskaźnika przechowywanego na obiekt, tak jak:
W takim przypadku koszt pamięci jest stosunkowo ogromny. Rzeczywisty rozmiar pamięci instancji klasy będzie teraz często wyglądał następująco na architekturach 64-bitowych:
Łącznie dla tej
Integer
klasy jest 16 bajtów, w przeciwieństwie do zaledwie 4 bajtów. Jeśli przechowujemy milion z nich w macierzy, otrzymamy 16 megabajtów pamięci: dwa razy większy niż typowy 8 MB pamięci podręcznej procesora L3, a powtarzanie tej macierzy wielokrotnie może być wielokrotnie wolniejsze niż ekwiwalent 4 megabajtów bez wirtualnego wskaźnika w wyniku dodatkowych braków pamięci podręcznej i błędów strony.Koszt wirtualnego wskaźnika na obiekt nie wzrasta jednak wraz z większą liczbą funkcji wirtualnych. Możesz mieć 100 wirtualnych funkcji członka w klasie, a narzut na instancję nadal byłby pojedynczym wirtualnym wskaźnikiem.
Wirtualny wskaźnik jest zwykle bardziej bezpośrednim problemem z ogólnego punktu widzenia. Jednak oprócz wirtualnego wskaźnika na instancję jest koszt na klasę. Każda klasa z funkcjami wirtualnymi generuje
vtable
w pamięci, która przechowuje adresy funkcji, które powinna faktycznie wywoływać (dyspozycja wirtualna / dynamiczna), gdy wykonywane jest wywołanie funkcji wirtualnej.vptr
Przechowywane na przykład wtedy punkty do tej klasy specyficznychvtable
. Narzut ten zwykle stanowi mniejszy problem, ale może nadmuchać rozmiar pliku binarnego i dodać trochę kosztów w czasie wykonywania, jeśli koszty te zostały niepotrzebnie zapłacone za tysiąc klas w złożonej bazie kodu, np. Tavtable
strona kosztu faktycznie rośnie proporcjonalnie z większą liczbą i więcej funkcji wirtualnych w miksie.Programiści Java pracujący w obszarach krytycznych pod względem wydajności bardzo dobrze rozumieją tego rodzaju koszty ogólne (choć często opisywane w kontekście boksu), ponieważ typ zdefiniowany przez użytkownika Java domyślnie dziedziczy z centralnej
object
klasy bazowej, a wszystkie funkcje w Javie są domyślnie wirtualne (możliwe do zastąpienia ) z natury, chyba że zaznaczono inaczej. W rezultacie JavaInteger
również wymaga 16 bajtów pamięci na platformach 64-bitowych ze względu navptr
metadane związane ze stylem na instancję i zazwyczaj nie jest możliwe zawinięcie w Javę czegoś takiego jak pojedynczaint
klasa bez płacenia za środowisko uruchomieniowe koszt wydajności.C ++ naprawdę faworyzuje wydajność dzięki podejściu typu „pay as you go”, a także wciąż wielu projektom opartym na sprzęcie metalowym odziedziczonym po C. Nie chce niepotrzebnie uwzględniać kosztów ogólnych związanych z generowaniem vtable i dynamicznym wysyłaniem każda zaangażowana klasa / instancja. Jeśli wydajność nie jest jednym z kluczowych powodów, dla których używasz języka takiego jak C ++, możesz odnieść większe korzyści z innych języków programowania, ponieważ duża część języka C ++ jest mniej bezpieczna i trudniejsza, niż byłoby to możliwe w przypadku często występującej wydajności kluczowy powód, aby faworyzować taki projekt.
Całkiem często. Jeśli klasa nie jest zaprojektowana do dziedziczenia, to nie potrzebuje wirtualnego destruktora i ostatecznie zapłaciłby tylko potencjalnie duży koszt za coś, czego nie potrzebuje. Podobnie, nawet jeśli klasa jest zaprojektowana do dziedziczenia, ale nigdy nie usuwasz instancji podtypu za pomocą wskaźnika bazowego, to również nie wymaga wirtualnego destruktora. W takim przypadku bezpieczną praktyką jest zdefiniowanie chronionego niewirtualnego destruktora, takiego jak:
W rzeczywistości łatwiej jest pokryć, kiedy powinieneś używać wirtualnych niszczycieli. Dość często znacznie więcej klas w twojej bazie kodu nie będzie zaprojektowanych do dziedziczenia.
std::vector
, na przykład, nie jest przeznaczony do dziedziczenia i zwykle nie powinien być dziedziczony (bardzo chwiejny projekt), ponieważ będzie to podatne na ten problem usuwania wskaźnika podstawowego (std::vector
umyślnie omija wirtualny destruktor) oprócz niezręcznych problemów z wycinaniem obiektów , jeśli twój klasa pochodna dodaje dowolny nowy stan.Ogólnie rzecz biorąc, dziedziczona klasa powinna mieć albo publiczny wirtualny niszczyciel, albo chroniony, niewirtualny. Z
C++ Coding Standards
rozdziału 50:Jedną z rzeczy, które C ++ zwykle podkreśla w sposób dorozumiany (ponieważ projekty stają się naprawdę kruche i niewygodne, a być może nawet niebezpieczne inaczej) jest idea, że dziedziczenie nie jest mechanizmem zaprojektowanym do późniejszej refleksji. Jest to mechanizm rozszerzalności z uwzględnieniem polimorfizmu, ale taki, który wymaga przewidywania, gdzie jest potrzebna rozszerzalność. W rezultacie twoje klasy podstawowe powinny być zaprojektowane jako pierwiastki hierarchii dziedziczenia z góry, a nie coś, co odziedziczysz później, bez uprzedzenia.
W tych przypadkach, w których po prostu chcesz odziedziczyć do ponownego użycia istniejącego kodu, często zaleca się skład (Zasada złożonego ponownego użycia).
źródło
Dlaczego c ++ nie ustawia domyślnie wirtualnych wszystkich destruktorów? Koszt dodatkowej przestrzeni dyskowej i wywołania tabeli metod wirtualnych. C ++ jest używany do programowania systemowego, rt o niskim opóźnieniu, gdzie może to być obciążenie.
źródło
To dobry przykład, kiedy nie używać wirtualnego destruktora: Od Scott Meyers:
Jeśli klasa nie zawiera żadnych funkcji wirtualnych, często oznacza to, że nie ma być używana jako klasa podstawowa. Kiedy klasa nie jest przeznaczona do użycia jako klasa podstawowa, wirtualny destruktor jest zwykle złym pomysłem. Rozważ ten przykład na podstawie dyskusji w ARM:
Jeśli short int zajmuje 16 bitów, obiekt Point może zmieścić się w rejestrze 32-bitowym. Ponadto obiekt Point można przekazać jako 32-bitową liczbę do funkcji napisanych w innych językach, takich jak C lub FORTRAN. Jeśli wirtualny punktator jest wirtualny, sytuacja się zmienia.
W momencie dodawania elementu wirtualnego do klasy dodawany jest wirtualny wskaźnik wskazujący na wirtualną tabelę dla tej klasy.
źródło
If a class does not contain any virtual functions, that is often an indication that it is not meant to be used as a base class.
Wut Czy ktoś jeszcze pamięta dobre stare dni, w których mogliśmy wykorzystywać klasy i dziedzictwo do budowania kolejnych warstw członków i zachowań wielokrotnego użytku, bez konieczności dbania o metody wirtualne? No dalej, Scott. Rozumiem sedno, ale to „często” naprawdę sięga.Wirtualny destruktor dodaje koszty działania. Koszt jest szczególnie duży, jeśli klasa nie ma żadnych innych metod wirtualnych. Wirtualny destruktor jest także potrzebny tylko w jednym konkretnym scenariuszu, w którym obiekt jest usuwany lub w inny sposób niszczony przez wskaźnik do klasy podstawowej. W takim przypadku destruktor klasy podstawowej musi być wirtualny, a destruktor dowolnej klasy pochodnej będzie domyślnie wirtualny. Istnieje kilka scenariuszy, w których polimorficzna klasa bazowa jest używana w taki sposób, że destruktor nie musi być wirtualny:
std::unique_ptr<Derived>
, a polimorfizm zachodzi tylko za pomocą wskaźników i referencji niebędących właścicielami. Innym przykładem jest przydzielanie obiektów za pomocąstd::make_shared<Derived>()
. Można używaćstd::shared_ptr<Base>
tak długo, jak początkowy wskaźnik tostd::shared_ptr<Derived>
. Wynika to z faktu, że wspólne wskaźniki mają własną dynamiczną dyspozycję dla destruktorów (separator), która niekoniecznie polega na wirtualnym destruktorze klasy bazowej.Oczywiście każdą konwencję używania obiektów tylko w wyżej wspomniany sposób można łatwo złamać. Dlatego rada Herb Suttera pozostaje tak aktualna jak zawsze: „Destruktory klasy podstawowej powinny być publiczne i wirtualne lub chronione i nie wirtualne”. W ten sposób, jeśli ktoś spróbuje usunąć wskaźnik do klasy podstawowej za pomocą nie-wirtualnego destruktora (-ów), najprawdopodobniej otrzyma błąd naruszenia zasad dostępu w czasie kompilacji.
Z drugiej strony istnieją klasy, które nie są zaprojektowane jako (publiczne) klasy podstawowe. Moje osobiste zalecenie to zrobić je
final
w C ++ 11 lub wyższej. Jeśli jest zaprojektowany jako kwadratowy kołek, istnieje duże prawdopodobieństwo, że nie będzie działał dobrze jako okrągły kołek. Jest to związane z moją preferencją posiadania jawnego kontraktu dziedziczenia między klasą podstawową a klasą pochodną, wzorcem projektowym NVI (interfejs niebędący interfejsem wirtualnym), dla abstrakcyjnych, a nie konkretnych klas bazowych, a także z moją niechęcią do chronionych zmiennych składowych, między innymi , ale wiem, że wszystkie te poglądy są do pewnego stopnia kontrowersyjne.źródło
Zadeklarowanie destruktora
virtual
jest konieczne tylko wtedy, gdy planujesz uczynićclass
dziedziczonym. Zazwyczaj klasy biblioteki standardowej (takie jakstd::string
) nie zapewniają wirtualnego destruktora, a zatem nie są przeznaczone do podklasowania.źródło
delete
wskaźnika do klasy bazowej.W konstruktorze pojawi się narzut związany z tworzeniem vtable (jeśli nie masz innych funkcji wirtualnych, w takim przypadku PRAWDOPODOBNIE, ale nie zawsze, powinieneś także mieć wirtualny destruktor). A jeśli nie masz żadnych innych funkcji wirtualnych, powoduje to, że Twój obiekt jest o jeden rozmiar większy niż jest to konieczne. Oczywiście zwiększony rozmiar może mieć duży wpływ na małe obiekty.
Odczytywana jest dodatkowa pamięć, aby uzyskać tabelę vtable, a następnie wywołać funkcję niebezpośrednią przez to, co jest narzutem na nie-wirtualny destruktor, gdy wywoływany jest destruktor. I oczywiście w konsekwencji generowany jest dodatkowy kod dla każdego wywołania destruktora. Dzieje się tak w przypadkach, w których kompilator nie może wydedukować rzeczywistego typu - w przypadkach, w których może wydedukować rzeczywisty typ, kompilator nie użyje vtable, ale wywoła bezpośrednio destruktor.
Państwo powinno mieć wirtualny destruktor jeśli klasa ma służyć jako klasy bazowej, zwłaszcza jeśli może to być tworzone / zniszczone za pośrednictwem innego podmiotu niż kod, który wie, jakiego typu jest to w stworzeniu, to trzeba wirtualnego destruktora.
Jeśli nie jesteś pewien, użyj wirtualnego destruktora. Łatwiej jest usunąć wirtualny, jeśli pojawia się jako problem, niż próbować znaleźć błąd spowodowany przez „brak wywołania odpowiedniego destruktora”.
Krótko mówiąc, nie powinieneś mieć wirtualnego destruktora, jeśli: 1. Nie masz żadnych funkcji wirtualnych. 2. Nie wyprowadzaj się z klasy (zaznacz ją
final
w C ++ 11, w ten sposób kompilator powie, czy spróbujesz z niej wywnioskować).W większości przypadków tworzenie i niszczenie nie stanowi znacznej części czasu spędzonego na użyciu konkretnego obiektu, chyba że istnieje „dużo treści” (utworzenie ciągu 1 MB zajmie oczywiście trochę czasu, ponieważ co najmniej 1 MB danych musi kopiowane z dowolnego miejsca). Zniszczenie ciągu 1 MB nie jest gorsze niż zniszczenie ciągu 150B, oba będą wymagać cofnięcia przydziału pamięci ciągu, i niewiele więcej, więc czas spędzony tam jest zwykle taki sam [chyba że jest to kompilacja debugowania, w której zwolnienie często wypełnia pamięć „wzór trucizny” - ale nie w ten sposób uruchomisz swoją prawdziwą aplikację w produkcji].
Krótko mówiąc, jest niewielki nad głową, ale w przypadku małych obiektów może to mieć znaczenie.
Zauważ też, że w niektórych przypadkach kompilatory mogą zoptymalizować wirtualne wyszukiwanie, więc jest to tylko kara
Jak zawsze, jeśli chodzi o wydajność, zużycie pamięci itp .: Test porównawczy i profil oraz pomiary, porównanie wyników z alternatywami i sprawdzenie, gdzie spędza się NAJWIĘKSZY czas / pamięć, i nie próbuj optymalizować 90% kod, który nie jest uruchamiany zbyt często [większość aplikacji ma około 10% kodu, który ma duży wpływ na czas wykonywania i 90% kodu, który nie ma większego wpływu]. Zrób to na wysokim poziomie optymalizacji, aby kompilator miał już dobrą robotę! I powtórz, sprawdź ponownie i popraw krok po kroku. Nie staraj się być sprytnym i staraj się dowiedzieć, co jest ważne, a co nie, chyba że masz duże doświadczenie z danym rodzajem aplikacji.
źródło
You **should** have a virtual destructor if your class is intended as a base-class
jest to rażące uproszczenie - i przedwczesna pesymizacja . Jest to potrzebne tylko wtedy, gdy ktoś może usunąć klasę pochodną za pomocą wskaźnika do bazy. W wielu sytuacjach tak nie jest. Jeśli wiesz, że tak, to z pewnością poniesiesz koszty ogólne. Które, btw, jest zawsze dodawane, nawet jeśli rzeczywiste wywołania mogą być rozwiązane statycznie przez kompilator. W przeciwnym razie, kiedy właściwie kontrolujesz, co ludzie mogą zrobić z twoimi przedmiotami, nie warto