Ich glaubte, ich habe viele Male nach virtuellen Destruktoren gesucht, die meisten erwähnen den Zweck von virtuellen Destruktoren und warum Sie virtuelle Destruktoren benötigen. Außerdem denke ich, dass Destruktoren in den meisten Fällen virtuell sein müssen.
Dann lautet die Frage: Warum setzt c ++ nicht standardmäßig alle Destruktoren virtuell? oder bei anderen fragen:
Wann brauche ich keine virtuellen Destruktoren?
In welchem Fall sollte ich keine virtuellen Destruktoren verwenden?
Was kostet die Verwendung virtueller Destruktoren, wenn ich sie verwende, auch wenn sie nicht benötigt werden?
c++
virtual-functions
ggrr
quelle
quelle
Antworten:
Wenn Sie einer Klasse einen virtuellen Destruktor hinzufügen:
In den meisten (allen?) aktuellen C ++ - Implementierungen muss jede Objektinstanz dieser Klasse einen Zeiger auf die virtuelle Versandtabelle für den Laufzeittyp speichern und diese virtuelle Versandtabelle selbst dem ausführbaren Image hinzufügen
Die Adresse der virtuellen Verteilertabelle ist nicht unbedingt prozessübergreifend gültig. Dies kann die sichere Freigabe solcher Objekte im gemeinsam genutzten Speicher verhindern
Ein eingebetteter virtueller Zeiger frustriert das Erstellen einer Klasse mit einem Speicherlayout, das einem bekannten Eingabe- oder Ausgabeformat entspricht (z. B.
Price_Tick*
könnte ein Speicher direkt auf einen geeignet ausgerichteten Speicher in einem eingehenden UDP-Paket ausgerichtet und zum Parsen / Zugreifen oder Ändern der Daten verwendet werden) Platzieren einernew
solchen Klasse, um Daten in ein ausgehendes Paket zu schreiben)Die Destruktor-Aufrufe selbst müssen möglicherweise - unter bestimmten Umständen - virtuell und daher offline gesendet werden, wohingegen nicht-virtuelle Destruktoren möglicherweise inline geschaltet oder optimiert werden, wenn dies für den Aufrufer trivial oder irrelevant ist
Das Argument "nicht entworfen, um von geerbt zu werden" wäre kein praktischer Grund dafür, nicht immer einen virtuellen Destruktor zu haben, wenn es nicht auch in praktischer Weise schlechter wäre, wie oben erläutert; Angesichts der Tatsache, dass es schlimmer ist, ist dies ein wichtiges Kriterium für die Kostenübernahme: Standardmäßig wird ein virtueller Destruktor verwendet, wenn Ihre Klasse als Basisklasse verwendet werden soll . Dies ist nicht immer erforderlich, stellt jedoch sicher, dass die Klassen in der Hierarchie ohne unbeabsichtigtes undefiniertes Verhalten freier verwendet werden können, wenn ein abgeleiteter Klassendestruktor mithilfe eines Basisklassenzeigers oder einer Referenz aufgerufen wird.
Nicht so ... viele Klassen haben keinen solchen Bedarf. Es gibt so viele Beispiele, bei denen es unnötig ist, sie aufzuzählen. Schauen Sie sich einfach Ihre Standardbibliothek an oder sagen Sie boost, und Sie werden feststellen, dass es eine große Mehrheit von Klassen gibt, die keine virtuellen Destruktoren haben. In Boost 1.53 zähle ich 72 von 494 virtuellen Destruktoren.
quelle
Übrigens,
Für eine Basisklasse mit polymorpher Löschung.
quelle
Die Kosten für die Einführung einer virtuellen Funktion in eine Klasse (geerbt oder Teil der Klassendefinition) sind möglicherweise sehr hoch (oder nicht abhängig vom Objekt), wenn ein virtueller Zeiger pro Objekt gespeichert wird.
In diesem Fall sind die Speicherkosten relativ groß. Die tatsächliche Speichergröße einer Klasseninstanz sieht auf 64-Bit-Architekturen jetzt häufig folgendermaßen aus:
Die Gesamtsumme für diese
Integer
Klasse beträgt 16 Bytes im Gegensatz zu nur 4 Bytes. Wenn wir eine Million davon in einem Array speichern, werden 16 Megabyte Arbeitsspeicher verbraucht: die doppelte Größe des typischen 8 MB L3-CPU-Caches, und das wiederholte Durchlaufen eines solchen Arrays kann um ein Vielfaches langsamer sein als das 4-Megabyte-Äquivalent ohne den virtuellen Zeiger infolge zusätzlicher Cache-Fehlschläge und Seitenfehler.Diese Kosten für virtuelle Zeiger pro Objekt steigen jedoch nicht mit mehr virtuellen Funktionen. Sie können 100 virtuelle Memberfunktionen in einer Klasse haben, und der Overhead pro Instanz wäre immer noch ein einzelner virtueller Zeiger.
Der virtuelle Zeiger ist vom Overhead-Standpunkt aus in der Regel das unmittelbarere Problem. Zusätzlich zu einem virtuellen Zeiger pro Instanz fallen jedoch Kosten pro Klasse an. Jede Klasse mit virtuellen Funktionen generiert einen
vtable
In-Memory-Speicher, in dem Adressen zu den Funktionen gespeichert sind, die sie bei einem virtuellen Funktionsaufruf tatsächlich aufrufen soll (virtueller / dynamischer Versand). Dievptr
pro Instanz gespeicherte zeigt dann auf diese klassenspezifischevtable
. Dieser Overhead ist normalerweise weniger bedenklich, aber er kann Ihre Binärgrößevtable
erhöhen und ein wenig Laufzeitkosten verursachen, wenn dieser Overhead für tausend Klassen in einer komplexen Codebasis unnötig gezahlt wird, z mehr virtuelle funktionen im mix.Java-Entwickler, die in leistungskritischen Bereichen arbeiten, verstehen diese Art von Overhead sehr gut (obwohl dies häufig im Zusammenhang mit dem Boxen beschrieben wird), da ein benutzerdefinierter Java-Typ implizit von einer zentralen
object
Basisklasse erbt und alle Funktionen in Java implizit virtuell (überschreibbar) sind ) in der Natur, sofern nicht anders angegeben. Infolgedessen benötigt ein JavaInteger
auf 64-Bit-Plattformen invptr
der Regel 16 Byte Speicher aufgrund dieser pro Instanz zugeordneten Metadaten. In Java ist es normalerweise unmöglich, so etwas wie eine einzelneint
in eine Klasse zu packen, ohne dafür eine Laufzeit zu bezahlen Leistungskosten dafür.C ++ begünstigt die Leistung mit einer Art "Pay-as-you-go" -Mentalente und einer Menge von Bare-Metal-Hardware-basierten Designs, die von C geerbt wurden. Es soll nicht unnötig den Overhead beinhalten, der für die Erstellung von virtuellen Tabellen und den dynamischen Versand für erforderlich ist jede einzelne betroffene Klasse / Instanz. Wenn die Leistung nicht einer der Hauptgründe ist, warum Sie eine Sprache wie C ++ verwenden, können Sie möglicherweise mehr von anderen Programmiersprachen profitieren, da ein Großteil der C ++ - Sprache weniger sicher und schwieriger ist, als dies im Idealfall häufig der Fall ist der Hauptgrund für ein solches Design.
Ziemlich oft. Wenn eine Klasse nicht für die Vererbung konzipiert ist, benötigt sie keinen virtuellen Destruktor und würde nur einen möglicherweise hohen Aufwand für etwas bezahlen, das sie nicht benötigt. Auch wenn eine Klasse so konzipiert ist, dass sie geerbt wird, Sie jedoch Subtyp-Instanzen niemals über einen Basiszeiger löschen, ist kein virtueller Destruktor erforderlich. In diesem Fall ist es eine sichere Praxis, einen geschützten nicht-virtuellen Destruktor wie folgt zu definieren:
Es ist tatsächlich leichte Abdeckung , wenn Sie sollten virtuelle Destruktoren verwenden. Sehr oft sind weit mehr Klassen in Ihrer Codebasis nicht für die Vererbung vorgesehen.
std::vector
Zum Beispiel ist es nicht so konzipiert, dass es vererbt wird, und sollte normalerweise nicht vererbt werden (sehr wackeliges Design), da diesstd::vector
zusätzlich zu Problemen beim Aufteilen von Objekten zu Problemen beim Löschen von Basiszeigern führt ( ein virtueller Destruktor wird absichtlich vermieden) Die abgeleitete Klasse fügt jeden neuen Status hinzu.Im Allgemeinen sollte eine geerbte Klasse entweder einen öffentlichen virtuellen Destruktor oder einen geschützten, nicht virtuellen Destruktor haben. Ab
C++ Coding Standards
Kapitel 50:Eines der Dinge, die C ++ implizit hervorhebt (weil Entwürfe dazu neigen, sehr spröde und umständlich zu werden und möglicherweise auch sonst unsicher zu werden), ist die Idee, dass Vererbung kein Mechanismus ist, der als nachträglicher Gedanke gedacht ist. Es ist ein Erweiterungsmechanismus mit Polymorphismus im Auge, der jedoch Voraussicht erfordert, wo Erweiterbarkeit erforderlich ist. Infolgedessen sollten Ihre Basisklassen im Voraus als Wurzeln einer Vererbungshierarchie entworfen werden und nicht als Erbe von später als nachträglicher Einfall ohne eine solche Vorausschau.
In den Fällen, in denen Sie lediglich den vorhandenen Code übernehmen möchten, wird die Komposition häufig stark empfohlen (Prinzip der kombinierten Wiederverwendung).
quelle
Warum setzt c ++ nicht standardmäßig alle Destruktoren auf virtuell? Kosten für zusätzlichen Speicher und Aufruf der virtuellen Methodentabelle. C ++ wird für System-RT-Programmierung mit geringer Latenz verwendet, wo dies eine Belastung darstellen könnte.
quelle
Dies ist ein gutes Beispiel dafür, wann der virtuelle Destruktor nicht verwendet werden sollte: Von Scott Meyers:
Wenn eine Klasse keine virtuellen Funktionen enthält, ist dies oft ein Hinweis darauf, dass sie nicht als Basisklasse verwendet werden soll. Wenn eine Klasse nicht als Basisklasse verwendet werden soll, ist es normalerweise eine schlechte Idee, den Destruktor virtuell zu machen. Betrachten Sie dieses Beispiel anhand einer Diskussion im ARM:
Wenn ein kurzes int 16 Bit belegt, kann ein Point-Objekt in ein 32-Bit-Register passen. Darüber hinaus kann ein Point-Objekt als 32-Bit-Menge an Funktionen übergeben werden, die in anderen Sprachen wie C oder FORTRAN geschrieben wurden. Wenn der Destruktor von Point jedoch virtuell gemacht wird, ändert sich die Situation.
In dem Moment, in dem Sie ein virtuelles Mitglied hinzufügen, wird ein virtueller Zeiger zu Ihrer Klasse hinzugefügt, der auf die virtuelle Tabelle für diese Klasse zeigt.
quelle
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. Erinnert sich noch jemand an die guten alten Zeiten, in denen wir mithilfe von Klassen und Vererbung aufeinanderfolgende Ebenen wiederverwendbarer Elemente und Verhaltensweisen aufbauen durften, ohne uns um virtuelle Methoden kümmern zu müssen? Komm schon, Scott. Ich verstehe den Kernpunkt, aber das "oft" ist wirklich zu erreichen.Ein virtueller Destruktor erhöht die Laufzeitkosten. Die Kosten sind besonders hoch, wenn die Klasse keine anderen virtuellen Methoden hat. Der virtuelle Destruktor wird auch nur in einem bestimmten Szenario benötigt, in dem ein Objekt durch einen Zeiger auf eine Basisklasse gelöscht oder auf andere Weise zerstört wird. In diesem Fall muss der Basisklassendestruktor virtuell sein, und der Destruktor einer abgeleiteten Klasse ist implizit virtuell. Es gibt einige Szenarien, in denen eine polymorphe Basisklasse so verwendet wird, dass der Destruktor nicht virtuell sein muss:
std::unique_ptr<Derived>
. Ein weiteres Beispiel ist die Zuweisung von Objekten mitstd::make_shared<Derived>()
. Es ist in Ordnung zu verwenden,std::shared_ptr<Base>
solange der anfängliche Zeiger a warstd::shared_ptr<Derived>
. Dies liegt daran, dass freigegebene Zeiger einen eigenen dynamischen Versand für Destruktoren (den Deleter) haben, der nicht unbedingt auf einem Destruktor für virtuelle Basisklassen beruht.Natürlich kann jede Konvention, Objekte nur auf die oben genannten Arten zu verwenden, leicht gebrochen werden. Daher bleibt Herb Sutters Rat wie immer gültig: "Destruktoren der Basisklasse sollten entweder öffentlich und virtuell oder geschützt und nicht virtuell sein." Wenn jemand versucht, einen Zeiger auf eine Basisklasse mit nicht-virtuellem Destruktor zu löschen, wird er auf diese Weise höchstwahrscheinlich zur Kompilierungszeit einen Zugriffsverletzungsfehler erhalten.
Andererseits gibt es Klassen, die nicht als (öffentliche) Basisklassen konzipiert sind. Meine persönliche Empfehlung ist, sie
final
in C ++ 11 oder höher zu erstellen. Wenn es sich um einen quadratischen Stift handelt, funktioniert es wahrscheinlich nicht so gut wie ein runder Stift. Dies hängt unter anderem mit meiner Präferenz für einen expliziten Vererbungsvertrag zwischen Basisklasse und abgeleiteter Klasse, für das NVI-Entwurfsmuster (non-virtual interface) für abstrakte statt für konkrete Basisklassen und meiner Abneigung gegen geschützte Mitgliedsvariablen zusammen , aber ich weiß, dass all diese Ansichten bis zu einem gewissen Grad umstritten sind.quelle
Das Deklarieren eines Destruktors
virtual
ist nur dann erforderlich, wenn Sie vorhaben, den Destruktorclass
vererbbar zu machen . In der Regel stellen die Klassen der Standardbibliothek (z. B.std::string
) keinen virtuellen Destruktor zur Verfügung und sind daher nicht für die Unterklasse gedacht.quelle
delete
eines Zeigers auf eine Basisklasse.Das Erstellen der vtable ist im Konstruktor mit einem Mehraufwand verbunden (wenn Sie keine anderen virtuellen Funktionen haben, sollten Sie in diesem Fall wahrscheinlich, aber nicht immer, auch einen virtuellen Destruktor haben). Und wenn Sie keine anderen virtuellen Funktionen haben, wird Ihr Objekt um eine Zeigergröße größer, als es sonst erforderlich ist. Offensichtlich kann die vergrößerte Größe große Auswirkungen auf kleine Objekte haben.
Es gibt einen zusätzlichen Speicher, der gelesen wird, um die Vtable abzurufen und dann die Funktion indirekt aufzurufen. Dies ist ein Overhead für den nicht-virtuellen Destruktor, wenn der Destruktor aufgerufen wird. Und natürlich wird in der Folge für jeden Aufruf des Destruktors ein wenig zusätzlicher Code generiert. Dies gilt für Fälle, in denen der Compiler den tatsächlichen Typ nicht ableiten kann. In den Fällen, in denen er den tatsächlichen Typ ableiten kann, verwendet der Compiler nicht die vtable, sondern ruft den Destruktor direkt auf.
Sie sollten einen virtuellen Destruktor haben, wenn Ihre Klasse als Basisklasse gedacht ist, insbesondere wenn sie von einer anderen Entität als dem Code erstellt / zerstört werden kann, der weiß, welchen Typ sie bei der Erstellung hat. Dann benötigen Sie einen virtuellen Destruktor.
Wenn Sie sich nicht sicher sind, verwenden Sie den virtuellen Destruktor. Es ist einfacher, Virtual zu entfernen, wenn es sich um ein Problem handelt, als zu versuchen, den Fehler zu finden, der durch "Der richtige Destruktor wird nicht aufgerufen" verursacht wird.
Kurz gesagt, Sie sollten keinen virtuellen Destruktor haben, wenn: 1. Sie keine virtuellen Funktionen haben. 2. Leiten Sie nicht von der Klasse ab (markieren Sie sie
final
in C ++ 11, damit der Compiler erkennt, ob Sie versuchen, davon abzuleiten).In den meisten Fällen wird für das Erstellen und Löschen eines Objekts nur dann viel Zeit aufgewendet, wenn "viel Inhalt" vorhanden ist (das Erstellen eines 1-MB-Strings wird offensichtlich einige Zeit in Anspruch nehmen, da mindestens 1 MB Daten erforderlich sind kopiert werden, wo immer es sich gerade befindet). Das Zerstören eines 1-MB-Strings ist nicht schlimmer als das Zerstören eines 150-MB-Strings. Beide erfordern das Freigeben des String-Speichers und nicht viel mehr. Daher ist die dort verbrachte Zeit in der Regel gleich [es sei denn, es handelt sich um ein Debug-Build, bei dem das Freigeben häufig den Speicher mit a füllt "Giftmuster" - aber so werden Sie Ihre reale Anwendung nicht in der Produktion ausführen.
Kurz gesagt, es gibt einen geringen Overhead, aber bei kleinen Objekten kann dies einen Unterschied machen.
Beachten Sie auch, dass Compiler in einigen Fällen die virtuelle Suche optimieren können, sodass dies nur eine Strafe ist
Wie immer, wenn es um Leistung, Speicherbedarf und Ähnliches geht: Vergleiche die Ergebnisse mit Alternativen, vergleiche sie mit Profilen und Messungen und schaue nach, wo die meiste Zeit / der meiste Speicher verbraucht wird, und versuche nicht, die 90% von zu optimieren Code, der nicht viel ausgeführt wird [die meisten Anwendungen haben ungefähr 10% Code, der einen großen Einfluss auf die Ausführungszeit hat, und 90% Code, der überhaupt keinen großen Einfluss hat]. Tun Sie dies in einer hohen Optimierungsstufe, damit Sie bereits den Vorteil haben, dass der Compiler gute Arbeit leistet! Und wiederholen, erneut prüfen und schrittweise verbessern. Versuchen Sie nicht, klug zu sein und herauszufinden, was wichtig ist und was nicht, es sei denn, Sie haben viel Erfahrung mit dieser Art von Anwendung.
quelle
You **should** have a virtual destructor if your class is intended as a base-class
handelt es sich um eine grobe Vereinfachung - und vorzeitige Pessimisierung . Dies ist nur erforderlich, wenn eine abgeleitete Klasse durch einen Zeiger auf base gelöscht werden darf. In vielen Situationen ist das nicht so. Wenn Sie wissen , es ist, dann sicher, entstehen den Overhead. Was übrigens immer hinzugefügt wird, auch wenn die tatsächlichen Aufrufe vom Compiler statisch aufgelöst werden können. Andernfalls lohnt es sich nicht, genau zu steuern, was Personen mit Ihren Objekten tun können