Was kostet die Verwendung virtueller Destruktoren, wenn ich sie verwende, auch wenn sie nicht benötigt werden?
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.
struct Integer
{
virtual ~Integer() {}
int value;
};
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:
struct Integer
{
// 8 byte vptr overhead
int value; // 4 bytes
// typically 4 more bytes of padding for alignment of vptr
};
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). Die vptr
pro Instanz gespeicherte zeigt dann auf diese klassenspezifische vtable
. Dieser Overhead ist normalerweise weniger bedenklich, aber er kann Ihre Binärgröße vtable
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 Java Integer
auf 64-Bit-Plattformen in vptr
der Regel 16 Byte Speicher aufgrund dieser pro Instanz zugeordneten Metadaten. In Java ist es normalerweise unmöglich, so etwas wie eine einzelne int
in eine Klasse zu packen, ohne dafür eine Laufzeit zu bezahlen Leistungskosten dafür.
Dann lautet die Frage: Warum setzt c ++ nicht standardmäßig alle Destruktoren virtuell?
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.
Wann brauche ich keine virtuellen Destruktoren?
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:
class BaseClass
{
protected:
// Disallow deleting/destroying subclass objects through `BaseClass*`.
~BaseClass() {}
};
In welchem Fall sollte ich keine virtuellen Destruktoren verwenden?
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 dies std::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:
50. Machen Sie Basisklassen-Destruktoren öffentlich und virtuell oder geschützt und nicht virtuell. Löschen oder nicht löschen; Das ist die Frage: Wenn das Löschen durch einen Zeiger auf eine Base erlaubt sein soll, muss der Destruktor der Base öffentlich und virtuell sein. Ansonsten sollte es geschützt und nicht virtuell sein.
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).