Warum ist die Optimierung der leeren Basis verboten, wenn die leere Basisklasse auch eine Mitgliedsvariable ist?

14

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_assertwird scheitern. Wenn Sie entweder Foooder ändern, Barum stattdessen zu erben, Base2wird 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 Barist ein Typ und fooeine Mitgliedsvariable dieses Typs. Ich verstehe nicht, warum die Basisklasse von Bedeutung Barfür die Basisklasse des Typs fooist oder umgekehrt.

In der Tat würde ich, wenn überhaupt, erwarten, dass &foodies mit der Adresse der BarInstanz übereinstimmt, die es enthält - wie es in anderen Situationen erforderlich ist (1) . Schließlich mache ich nichts Besonderes mit virtualVererbung, die Basisklassen sind trotzdem leer, und die Kompilierung mit Base2zeigt, 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 StandardLayoutTypes obligatorisch (obwohl Baroben nicht a StandardLayoutType).

imallett
quelle
4
Wie wird die von Ihnen zitierte Begründung (" da die beiden Basis-Unterobjekte desselben Typs unterschiedliche Adressen haben müssen ") verfehlt? Unterschiedliche Objekte desselben Typs müssen unterschiedliche Adressen haben, und diese Anforderung stellt sicher, dass diese Regel nicht verletzt wird. Wenn hier eine leere Basisoptimierung angewendet wird, könnten wir Base *a = new Bar(); Base *b = a->foo;mit a==b, aber aund bsind eindeutig unterschiedliche Objekte (möglicherweise mit unterschiedlichen Überschreibungen virtueller Methoden).
Toby Speight
1
Die Antwort des Sprachrechtsanwalts zitiert die relevanten Teile der Spezifikation. Und anscheinend wissen Sie das bereits.
Deduplikator
3
Ich bin mir nicht sicher, ob ich verstehe, nach welcher Antwort Sie hier suchen. Das C ++ - Objektmodell ist das, was es ist. Die Einschränkung ist vorhanden, da das Objektmodell dies erfordert. Was suchen Sie darüber hinaus noch?
Nicol Bolas
@TobySpeight Verschiedene Objekte desselben Typs müssen unterschiedliche Adressen haben. Es ist leicht möglich, diese Regel in einem Programm mit genau definiertem Verhalten zu brechen.
Sprachanwalt
@TobySpeight Nein, ich meine nicht, dass Sie vergessen haben, über die Lebensdauer zu sagen: "Verschiedene Objekte des gleichen Typs mit ihrer Lebensdauer " . Es ist möglich, mehrere Objekte desselben Typs, die alle am Leben sind, an derselben Adresse zu haben. Es gibt mindestens 2 Fehler im Wortlaut, die dies ermöglichen.
Sprachanwalt

Antworten:

4

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)

Das nicht statische Datenelement kann die Adresse eines anderen nicht statischen Datenelements oder die einer Basisklasse [...] gemeinsam nutzen.

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:

int stupid_method(Base *b) {
  if( dynamic_cast<Foo*>(b) ) return 0;
  if( dynamic_cast<Bar*>(b) ) return 1;
  return 2;
}

Bar b;
stupid_method(&b);  // Would expect 0
stupid_method(&b.foo); //Would expect 1

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

void delBase(Base *b) {
    delete b;
}

Bar *b = new Bar;
delBase(b); // One would expect this to be absolutely fine.
delBase(&b->foo); // Whoaa, we shouldn't delete a member variable.

Aber wie sollte der Compiler diese beiden Fälle unterscheiden?

Und vielleicht ein bisschen weniger erfunden:

struct Base { 
  virtual void hi() { std::cout << "Hello\n";}
};

struct Foo : Base {
  void hi() override { std::cout << "Guten Tag\n";}
};

struct Bar : Base {
    Foo foo;
};

Bar b;
b.hi() // Hello
b.foo.hi() // Guten Tag
Base *a = &b;
Base *z = &b.foo;
a->hi() // Hello
z->hi() // Guten Tag

Aber die letzten beiden sind gleich, wenn wir eine leere Basisklassenoptimierung haben!

n314159
quelle
1
Man könnte jedoch argumentieren, dass der zweite Aufruf ohnehin ein undefiniertes Verhalten aufweist. Der Compiler muss also nichts unterscheiden.
StoryTeller - Unslander Monica
1
Eine Klasse mit virtuellen Mitgliedern ist nicht leer, also hier irrelevant!
Deduplikator
1
@Deduplicator Haben Sie ein Standardangebot dazu? Cppref sagt uns, dass eine leere Klasse "eine Klasse oder Struktur ist, die keine nicht statischen Datenelemente hat".
n314159
1
@ n314159 std::is_emptyauf cppreference ist weitaus ausgefeilter. Samt aus dem aktuellen Entwurf auf eel.is .
Deduplikator
2
Sie können nicht, dynamic_castwenn es nicht polymorph ist (mit kleinen Ausnahmen, die hier nicht relevant sind).
TC