Die Optimierung der leeren Basis ist großartig. Es gibt jedoch die folgende Einschränkung:
Eine leere Basisoptimierung ist verboten, wenn eine der leeren Basisklassen auch der Typ oder die Basis des Typs des ersten nicht statischen Datenelements ist, da die beiden Basisunterobjekte desselben Typs unterschiedliche Adressen innerhalb der Objektdarstellung haben müssen vom am meisten abgeleiteten Typ.
Beachten Sie den folgenden Code, um diese Einschränkung zu erklären. Das static_assert
wird scheitern. Wenn Sie entweder Foo
oder ändern, Bar
um stattdessen zu erben, Base2
wird der Fehler abgewendet:
#include <cstddef>
struct Base {};
struct Base2 {};
struct Foo : Base {};
struct Bar : Base {
Foo foo;
};
static_assert(offsetof(Bar,foo)==0,"Error!");
Ich verstehe dieses Verhalten vollständig. Was ich nicht verstehe, ist, warum dieses besondere Verhalten existiert . Es wurde offensichtlich aus einem Grund hinzugefügt, da es sich um eine explizite Ergänzung handelt, nicht um ein Versehen. Was ist ein Grund dafür?
Insbesondere, warum sollten die beiden Basis-Unterobjekte unterschiedliche Adressen haben müssen? Oben Bar
ist ein Typ und foo
eine Mitgliedsvariable dieses Typs. Ich verstehe nicht, warum die Basisklasse von Bedeutung Bar
für die Basisklasse des Typs foo
ist oder umgekehrt.
In der Tat würde ich, wenn überhaupt, erwarten, dass &foo
dies mit der Adresse der Bar
Instanz übereinstimmt, die es enthält - wie es in anderen Situationen erforderlich ist (1) . Schließlich mache ich nichts Besonderes mit virtual
Vererbung, die Basisklassen sind trotzdem leer, und die Kompilierung mit Base2
zeigt, dass in diesem speziellen Fall nichts kaputt geht.
Aber diese Argumentation ist eindeutig falsch, und es gibt andere Situationen, in denen diese Einschränkung erforderlich wäre.
Angenommen, die Antworten sollten für C ++ 11 oder neuer sein (ich verwende derzeit C ++ 17).
(1) Hinweis: EBO wurde in C ++ 11 aktualisiert und wurde insbesondere für StandardLayoutType
s obligatorisch (obwohl Bar
oben nicht a StandardLayoutType
).
quelle
Base *a = new Bar(); Base *b = a->foo;
mita==b
, abera
undb
sind eindeutig unterschiedliche Objekte (möglicherweise mit unterschiedlichen Überschreibungen virtueller Methoden).Antworten:
Ok, es scheint, als hätte ich es die ganze Zeit falsch gemacht, da für alle meine Beispiele eine vtable für das Basisobjekt vorhanden sein muss, die zunächst eine leere Basisoptimierung verhindern würde. Ich werde die Beispiele stehen lassen, da ich denke, dass sie einige interessante Beispiele dafür geben, warum eindeutige Adressen normalerweise eine gute Sache sind.
Nachdem Sie dies alles eingehender untersucht haben, gibt es keinen technischen Grund, die Optimierung der leeren Basisklasse zu deaktivieren, wenn das erste Mitglied vom gleichen Typ wie die leere Basisklasse ist. Dies ist nur eine Eigenschaft des aktuellen C ++ - Objektmodells.
In C ++ 20 gibt es jedoch ein neues Attribut
[[no_unique_address]]
, das dem Compiler mitteilt, dass ein nicht statisches Datenelement möglicherweise keine eindeutige Adresse benötigt (technisch gesehen überlappt es möglicherweise [intro.object] / 7 ).Dies impliziert, dass (Schwerpunkt meiner)
daher kann man die leere Basisklassenoptimierung "reaktivieren", indem man dem ersten Datenelement das Attribut gibt
[[no_unique_address]]
. Ich habe hier ein Beispiel hinzugefügt , das zeigt, wie dies (und alle anderen Fälle, die mir einfallen) funktioniert.Falsche Beispiele für Probleme dabei
Da es den Anschein hat, dass eine leere Klasse möglicherweise keine virtuellen Methoden hat, möchte ich ein drittes Beispiel hinzufügen:
Die letzten beiden Anrufe sind jedoch gleich.
Alte Beispiele (Beantworten Sie die Frage wahrscheinlich nicht, da leere Klassen anscheinend keine virtuellen Methoden enthalten)
Betrachten Sie in Ihrem obigen Code (mit hinzugefügten virtuellen Destruktoren) das folgende Beispiel
Aber wie sollte der Compiler diese beiden Fälle unterscheiden?
Und vielleicht ein bisschen weniger erfunden:
Aber die letzten beiden sind gleich, wenn wir eine leere Basisklassenoptimierung haben!
quelle
std::is_empty
auf cppreference ist weitaus ausgefeilter. Samt aus dem aktuellen Entwurf auf eel.is .dynamic_cast
wenn es nicht polymorph ist (mit kleinen Ausnahmen, die hier nicht relevant sind).