Was verhindert die Überlappung benachbarter Mitglieder in Klassen?

12

Betrachten Sie die folgenden drei structs:

class blub {
    int i;
    char c;

    blub(const blub&) {}
};

class blob {
    char s;

    blob(const blob&) {}
};

struct bla {
    blub b0;
    blob b1;
};

Auf typischen Plattformen mit int4 Bytes sind die Größen, Ausrichtungen und die Gesamtauffüllung 1 wie folgt:

  struct   size   alignment   padding  
 -------- ------ ----------- --------- 
  blub        8           4         3  
  blob        1           1         0  
  bla        12           4         6  

Es gibt keine Überlappung zwischen der Lagerung der blubund der blobElemente, obwohl die Größe 1 blobim Prinzip in die Polsterung von "passen" könnte blub.

In C ++ 20 wird das no_unique_addressAttribut eingeführt, mit dem benachbarte leere Mitglieder dieselbe Adresse verwenden können. Es erlaubt auch explizit das oben beschriebene Szenario, das Auffüllen eines Mitglieds zum Speichern eines anderen zu verwenden. Aus der Referenz (Hervorhebung von mir):

Gibt an, dass dieses Datenelement keine Adresse haben muss, die sich von allen anderen nicht statischen Datenelementen seiner Klasse unterscheidet. Dies bedeutet, dass der Compiler einen leeren Typ (z. B. zustandslosen Allocator) möglicherweise so optimiert, dass er keinen Speicherplatz belegt, genau wie wenn es sich um eine leere Basis handelt. Wenn das Element nicht leer ist, kann das darin enthaltene Tail-Padding auch zum Speichern anderer Datenelemente wiederverwendet werden.

In der Tat, wenn wir dieses Attribut auf verwenden blub b0, blafällt die Größe der Tropfen auf 8, so dass die blobin der Tat in der blub wie auf Godbolt gesehen gespeichert ist .

Zum Schluss kommen wir zu meiner Frage:

Welcher Text in den Standards (C ++ 11 bis C ++ 20) verhindert diese Überlappung ohne no_unique_addressfür Objekte, die nicht trivial kopierbar sind?

Ich muss trivial kopierbare (TC) Objekte von den oben genannten ausschließen, da es für TC-Objekte zulässig ist, std::memcpyvon einem Objekt zum anderen zu wechseln, einschließlich der Unterobjekte der Mitglieder, und wenn sich der Speicher überlappt, würde dies brechen (weil der gesamte oder ein Teil des Speichers für das benachbarte Mitglied würde überschrieben werden) 2 .


1 Wir berechnen die Polsterung einfach als Differenz zwischen der Strukturgröße und der Größe aller ihrer Bestandteile rekursiv.

2 Aus diesem Grund habe ich Kopierkonstruktoren definiert: zu machen blubund blobnicht trivial kopierbar .

BeeOnRope
quelle
Ich habe es nicht recherchiert, aber ich vermute die "als ob" -Regel. Wenn es keinen beobachtbaren Unterschied (übrigens ein Begriff mit sehr spezifischer Bedeutung) zur abstrakten Maschine gibt (gegen die Ihr Code kompiliert wird), kann der Compiler den Code nach Belieben ändern.
Jesper Juhl
Ich
bin
@JesperJuhl - richtig, aber ich frage, warum kann es nicht , nicht warum kann es , und die "als ob" -Regel gilt normalerweise für die erstere, macht aber für die letztere keinen Sinn. Außerdem ist "als ob" für das Strukturlayout nicht klar, was normalerweise ein globales Problem ist, kein lokales. Letztendlich muss der Compiler ein einziges konsistentes Regelwerk für das Layout haben, außer vielleicht für Strukturen, von denen er beweisen kann, dass sie niemals "entkommen".
BeeOnRope
1
@BeeOnRope Ich kann Ihre Frage leider nicht beantworten. Deshalb habe ich gerade einen Kommentar gepostet und keine Antwort. Was Sie in diesem Kommentar erhalten haben, war meine beste Vermutung für eine Erklärung, aber ich kenne die Antwort nicht (neugierig, sie selbst zu lernen - weshalb Sie eine positive Bewertung erhalten haben).
Jesper Juhl
1
@NicolBolas - antwortest du auf die richtige Frage? Hier geht es nicht darum, sichere Kopien oder etwas anderes zu erkennen. Ich bin eher neugierig, warum Polster zwischen Mitgliedern nicht wiederverwendet werden können. In jedem Fall liegen Sie falsch: Trivial kopierbar ist eine Eigenschaft des Typs und war es schon immer. Um ein Objekt sicher zu kopieren, muss es jedoch sowohl einen TC-Typ (eine Eigenschaft des Typs) als auch kein potenziell überlappendes Subjekt (eine Eigenschaft des Objekts, die Sie vermutlich verwirrt haben) haben. Ich weiß immer noch nicht, warum wir hier über Kopien sprechen.
BeeOnRope

Antworten:

1

Der Standard ist furchtbar leise, wenn es um das Speichermodell geht, und nicht sehr explizit über einige der verwendeten Begriffe. Aber ich denke, ich habe eine funktionierende Argumentation gefunden (die vielleicht etwas schwach ist)

Lassen Sie uns zunächst herausfinden, was überhaupt Teil eines Objekts ist. [basic.types] / 4 :

Die Objektdarstellung eines Objekts vom Typ Tist die Folge von N unsigned charObjekten, die vom Objekt vom Typ aufgenommen werden T, wobei Ngleich ist sizeof(T). Die Wertdarstellung eines Objekts vom Typ Tist die Menge von Bits, die an der Darstellung eines Wertes vom Typ beteiligt sind T. Bits in der Objektdarstellung, die nicht Teil der Wertdarstellung sind, sind Füllbits.

Die Objektdarstellung von b0besteht also aus sizeof(blub) unsigned charObjekten, also 8 Bytes. Die Füllbits sind Teil des Objekts.

Kein Objekt kann den Raum eines anderen einnehmen, wenn es nicht in ihm verschachtelt ist [basic.life] /1.5 :

Die Lebensdauer eines Objekts ovom Typ Tendet, wenn:

[...]

(1.5) Der Speicher, den das Objekt belegt, wird freigegeben oder von einem Objekt wiederverwendet, das nicht in o([intro.object]) verschachtelt ist .

Die Lebensdauer von b0würde also enden, wenn der von ihm belegte Speicher von einem anderen Objekt wiederverwendet würde, d b1. H. Ich habe das nicht überprüft, aber ich denke, der Standard schreibt vor, dass das Unterobjekt eines lebendigen Objekts auch lebendig sein sollte (und ich konnte mir nicht vorstellen, wie dies anders funktionieren sollte).

Der Speicher, der b0 belegt, darf also nicht von verwendet werden b1. Ich habe im Standard keine Definition von "besetzen" gefunden, aber ich denke, eine vernünftige Interpretation wäre "Teil der Objektdarstellung". In dem Zitat, das die Objektdarstellung beschreibt, werden die Wörter "aufnehmen" verwendet 1 . Hier wären dies 8 Bytes, blabenötigt also mindestens ein weiteres für b1.

Speziell für Unterobjekte (also unter anderem nicht statische Datenelemente) gibt es auch die Bedingung [intro.object] / 9 (dies wurde jedoch mit C ++ 20 hinzugefügt, thx @BeeOnRope)

Zwei Objekte mit überlappenden Lebensdauern, die keine Bitfelder sind, können dieselbe Adresse haben, wenn eines in das andere verschachtelt ist oder wenn mindestens eines ein Unterobjekt der Größe Null ist und sie unterschiedlichen Typs sind. Andernfalls haben sie unterschiedliche Adressen und belegen nicht zusammenhängende Speicherbytes .

(Hervorhebung von mir) Auch hier haben wir das Problem, dass "besetzt" nicht definiert ist, und ich würde erneut argumentieren, die Bytes in der Objektdarstellung zu verwenden. Beachten Sie, dass diese [basic.memobj] / Fußnote 29 eine Fußnote enthält

Nach der "Als-ob" -Regel darf eine Implementierung zwei Objekte an derselben Maschinenadresse speichern oder überhaupt kein Objekt speichern, wenn das Programm den Unterschied nicht beobachten kann ([intro.execution]).

Dies kann es dem Compiler ermöglichen, dies zu unterbrechen, wenn er nachweisen kann, dass keine beobachtbaren Nebenwirkungen vorliegen. Ich würde denken, dass dies für eine so grundlegende Sache wie das Objektlayout ziemlich kompliziert ist. Vielleicht wird diese Optimierung deshalb nur durchgeführt, wenn der Benutzer die Information bereitstellt, dass es keinen Grund gibt, durch Hinzufügen des [no_unique_address]Attributs disjunkte Objekte zu haben .

tl; dr: Polsterung kann Teil des Objekts sein und Mitglieder müssen disjunkt sein.


1 Ich konnte nicht widerstehen, eine Referenz hinzuzufügen, die besetzt bedeuten könnte: Webster's Revised Unabridged Dictionary, G. & C. Merriam, 1913 (Hervorhebung von mir)

  1. Um die Dimensionen von zu halten oder zu füllen; den Raum oder Raum von einnehmen ; zu bedecken oder zu füllen; Das Lager nimmt fünf Morgen Land ein. Sir J. Herschel.

Welcher Standard-Crawl wäre ohne einen Wörterbuch-Crawl vollständig?

n314159
quelle
2
Der Teil "disjunkte Speicherbytes belegen" von into.storage würde meiner Meinung nach ausreichen - aber dieser Wortlaut wurde nur in C ++ 20 als Teil der Änderung hinzugefügt, die hinzugefügt wurde no_unique_address. Es lässt die Situation vor C ++ 20 weniger klar. Ich habe Ihre Argumentation nicht verstanden, die dazu führte, dass "kein Objekt den Raum eines anderen belegen kann, wenn es nicht in ihm verschachtelt ist" aus basic.life/1.5, insbesondere, wie Sie aus "dem Speicher, den das Objekt belegt, freigegeben werden" gelangen. zu "kein Objekt kann den Raum eines anderen einnehmen".
BeeOnRope
1
Ich habe diesem Absatz eine kleine Klarstellung hinzugefügt. Ich hoffe das macht es verständlicher. Sonst werde ich es mir morgen nochmal ansehen, jetzt ist es ziemlich spät für mich.
n314159
"Zwei Objekte mit überlappenden Lebensdauern, die keine Bitfelder sind, können dieselbe Adresse haben, wenn eines in das andere verschachtelt ist oder wenn mindestens eines ein Unterobjekt der Größe Null ist und sie vom unterschiedlichen Typ sind." 2 Objekte mit überlappenden Lebensdauern, von der gleiche Typ, haben die gleiche Adresse .
Sprachanwalt
Entschuldigung, könnten Sie das näher erläutern? Sie zitieren ein Standardzitat aus meiner Antwort und bringen ein Beispiel mit, das ein wenig im Widerspruch dazu steht. Ich bin mir nicht sicher, ob dies ein Kommentar zu meiner Antwort ist und ob es das ist, was es mir sagen sollte. In Bezug auf Ihr Beispiel würde ich sagen, dass man noch andere Teile des Standards berücksichtigen musste (es gibt einen Absatz über ein vorzeichenloses Zeichenarray, das Speicher für ein anderes Objekt bereitstellt, etwas in Bezug auf die Basisoptimierung mit der Größe Null und noch weiter sollte man auch prüfen, ob die Platzierung neu ist Sonderzulagen, alle Dinge, von denen ich glaube, dass sie für das OP-Beispiel nicht relevant sind)
n314159
@ n314159 Ich denke, dieser Wortlaut könnte fehlerhaft sein.
Sprachanwalt