Warum kann ein T * im Register übergeben werden, ein unique_ptr <T> jedoch nicht?


85

Ich schaue mir Chandler Carruths Vortrag auf der CppCon 2019 an:

Es gibt keine kostengünstigen Abstraktionen

Darin gibt er das Beispiel, wie er überrascht war, wie viel Aufwand Ihnen durch die Verwendung eines std::unique_ptr<int>Over-An entsteht int*. Dieses Segment beginnt ungefähr zum Zeitpunkt 17:25.

Sie können sich die Kompilierungsergebnisse seines Beispiel-Snippet-Paares (godbolt.org) ansehen - um zu sehen, dass der Compiler tatsächlich nicht bereit zu sein scheint, den Wert unique_ptr zu übergeben - was in der Tat das Endergebnis ist Nur eine Adresse - in einem Register, nur im direkten Speicher.

Einer der Punkte, die Herr Carruth gegen 27:00 Uhr hervorhebt, ist, dass für den C ++ - ABI By-Value-Parameter (einige, aber nicht alle; vielleicht - nicht primitive Typen? Nicht trivial konstruierbare Typen?) Erforderlich sind, um im Speicher übergeben zu werden anstatt innerhalb eines Registers.

Meine Fragen:

  1. Ist dies auf einigen Plattformen tatsächlich eine ABI-Anforderung? (welche?) Oder ist es in bestimmten Szenarien nur eine Pessimierung?
  2. Warum ist der ABI so? Das heißt, wenn die Felder einer Struktur / Klasse in Register oder sogar in ein einzelnes Register passen - warum sollten wir es nicht in diesem Register übergeben können?
  3. Hat das C ++ - Standardkomitee diesen Punkt in den letzten Jahren oder jemals diskutiert?

PS - Um diese Frage nicht ohne Code zu hinterlassen:

Einfacher Zeiger:

void bar(int* ptr) noexcept;
void baz(int* ptr) noexcept;

void foo(int* ptr) noexcept {
    if (*ptr > 42) {
        bar(ptr); 
        *ptr = 42; 
    }
    baz(ptr);
}

Eindeutiger Zeiger:

using std::unique_ptr;
void bar(int* ptr) noexcept;
void baz(unique_ptr<int> ptr) noexcept;

void foo(unique_ptr<int> ptr) noexcept {
    if (*ptr > 42) { 
        bar(ptr.get());
        *ptr = 42; 
    }
    baz(std::move(ptr));
}

8
Ich bin nicht sicher, was die ABI-Anforderung genau ist, aber es verbietet nicht, Strukturen in Register zu setzen
Harold

6
Wenn ich raten müsste, würde ich sagen, dass es mit nicht trivialen Elementfunktionen zu tun hat, die einen thisZeiger benötigen , der auf eine gültige Position zeigt. unique_ptrhat die. Das Verschütten des Registers zu diesem Zweck würde die gesamte Optimierung "Durchlaufen eines Registers" irgendwie negieren.
StoryTeller - Unslander Monica

2
itanium-cxx-abi.github.io/cxx-abi/abi.html#calls . Also dieses Verhalten erforderlich. Warum? itanium-cxx-abi.github.io/cxx-abi/cxx-closed.html , suchen Sie nach dem Problem C-7. Dort gibt es einige Erklärungen, die jedoch nicht zu detailliert sind. Aber ja, dieses Verhalten erscheint mir nicht logisch. Diese Objekte könnten normal durch den Stapel geleitet werden. Es scheint eine Verschwendung zu sein, sie zum Stapeln zu schieben und dann die Referenz zu übergeben (nur für "nicht triviale" Objekte).
Geza

6
Es scheint, dass C ++ hier seine eigenen Prinzipien verletzt, was ziemlich traurig ist. Ich war zu 140% davon überzeugt, dass ein unique_ptr nach der Kompilierung einfach verschwindet. Immerhin ist es nur ein verzögerter Destruktoraufruf, der zur Kompilierungszeit bekannt ist.
One Man Monkey Squad

7
@ MaximEgorushkin: Wenn Sie es von Hand geschrieben hätten, hätten Sie den Zeiger in ein Register und nicht auf den Stapel gelegt.
Einpoklum

Antworten:


49
  1. Ist dies tatsächlich eine ABI-Anforderung oder ist es in bestimmten Szenarien nur eine Pessimierung?

Ein Beispiel ist AMD64 Architecture Processor Supplement für die binäre System V-Anwendungsschnittstelle . Dieser ABI ist für 64-Bit-x86-kompatible CPUs (Linux x86_64-Architektur) vorgesehen. Es folgt unter Solaris, Linux, FreeBSD, macOS, Windows Subsystem für Linux:

Wenn ein C ++ - Objekt entweder einen nicht trivialen Kopierkonstruktor oder einen nicht trivialen Destruktor hat, wird es als unsichtbare Referenz übergeben (das Objekt wird in der Parameterliste durch einen Zeiger mit der Klasse INTEGER ersetzt).

Ein Objekt mit einem nicht trivialen Kopierkonstruktor oder einem nicht trivialen Destruktor kann nicht als Wert übergeben werden, da solche Objekte genau definierte Adressen haben müssen. Ähnliche Probleme treten auf, wenn ein Objekt von einer Funktion zurückgegeben wird.

Beachten Sie, dass nur 2 Allzweckregister zum Übergeben von 1 Objekt mit einem Trivial-Copy-Konstruktor und einem Trivial-Destruktor verwendet werden können, dh, nur Werte von Objekten mit sizeofnicht mehr als 16 können in Registern übergeben werden. Siehe Aufrufkonventionen von Agner Fog für eine ausführliche Behandlung der Aufrufkonventionen, insbesondere §7.1 Passing und Objekte zurück. Es gibt separate Aufrufkonventionen zum Übergeben von SIMD-Typen in Registern.

Für andere CPU-Architekturen gibt es unterschiedliche ABIs.


  1. Warum ist der ABI so? Das heißt, wenn die Felder einer Struktur / Klasse in Register oder sogar in ein einzelnes Register passen - warum sollten wir es nicht in diesem Register übergeben können?

Es ist ein Implementierungsdetail, aber wenn eine Ausnahme behandelt wird, müssen während des Abwickelns des Stapels die Objekte mit der automatischen Speicherdauer, die zerstört werden, relativ zum Funktionsstapelrahmen adressierbar sein, da die Register zu diesem Zeitpunkt überlastet waren. Der Stapelabwicklungscode benötigt die Adressen von Objekten, um ihre Destruktoren aufzurufen, aber Objekte in Registern haben keine Adresse.

Pedantisch wirken Destruktoren auf Objekte :

Ein Objekt befindet sich während seiner Bauzeit ([class.cdtor]), während seiner gesamten Lebensdauer und während seiner Zerstörungszeit in einem Speicherbereich.

und ein Objekt kann in C ++ nicht existieren, wenn ihm kein adressierbarer Speicher zugewiesen ist, da die Identität des Objekts seine Adresse ist .

Wenn eine Adresse eines Objekts mit einem trivialen Kopierkonstruktor in Registern benötigt wird, kann der Compiler das Objekt einfach im Speicher speichern und die Adresse abrufen. Wenn der Kopierkonstruktor nicht trivial ist, kann der Compiler ihn andererseits nicht einfach im Speicher speichern, sondern muss den Kopierkonstruktor aufrufen, der eine Referenz verwendet und daher die Adresse des Objekts in den Registern benötigt. Die aufrufende Konvention kann wahrscheinlich nicht davon abhängen, ob der Kopierkonstruktor im Angerufenen eingefügt wurde oder nicht.

Eine andere Möglichkeit, darüber nachzudenken, besteht darin, dass der Compiler für trivial kopierbare Typen den Wert eines Objekts in Registern überträgt , aus denen ein Objekt bei Bedarf durch einfache Speicher wiederhergestellt werden kann. Z.B:

void f(long*);
void g(long a) { f(&a); }

auf x86_64 mit System V ABI kompiliert in:

g(long):                             // Argument a is in rdi.
        push    rax                  // Align stack, faster sub rsp, 8.
        mov     qword ptr [rsp], rdi // Store the value of a in rdi into the stack to create an object.
        mov     rdi, rsp             // Load the address of the object on the stack into rdi.
        call    f(long*)             // Call f with the address in rdi.
        pop     rax                  // Faster add rsp, 8.
        ret                          // The destructor of the stack object is trivial, no code to emit.

In seinem zum Nachdenken anregenden Vortrag erwähnt Chandler Carruth , dass eine brechende ABI-Änderung (unter anderem) notwendig sein könnte, um den destruktiven Schritt umzusetzen, der die Dinge verbessern könnte. IMO, die ABI-Änderung kann nicht unterbrochen werden, wenn sich die Funktionen, die das neue ABI verwenden, explizit für eine neue, andere Verknüpfung entscheiden, z. B. sie im extern "C++20" {}Block deklarieren (möglicherweise in einem neuen Inline-Namespace für die Migration vorhandener APIs). Damit nur der Code, der mit der neuen Verknüpfung gegen die neuen Funktionsdeklarationen kompiliert wurde, den neuen ABI verwenden kann.

Beachten Sie, dass ABI nicht angewendet wird, wenn die aufgerufene Funktion eingebunden wurde. Neben der Generierung von Link-Time-Code kann der Compiler Funktionen einbinden, die in anderen Übersetzungseinheiten definiert sind, oder benutzerdefinierte Aufrufkonventionen verwenden.


Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Samuel Liew

8

Bei gängigen ABIs kann ein nicht trivialer Destruktor -> keine Register übergeben

(Eine Illustration eines Punktes in der Antwort von @ MaximEgorushkin am Beispiel von @ harold in einem Kommentar; korrigiert gemäß dem Kommentar von @ Yakk.)

Wenn Sie kompilieren:

struct Foo { int bar; };
Foo test(Foo byval) { return byval; }

du erhältst:

test(Foo):
        mov     eax, edi
        ret

dh das FooObjekt wird testin einem register ( edi) übergeben und auch in einem register ( eax) zurückgegeben.

Wenn der Destruktor nicht trivial ist (wie im std::unique_ptrBeispiel von OPs) - Häufige ABIs müssen auf dem Stapel platziert werden. Dies gilt auch dann, wenn der Destruktor die Adresse des Objekts überhaupt nicht verwendet.

So können Sie auch im Extremfall eines Nicht-Zerstörers kompilieren:

struct Foo2 {
    int bar;
    ~Foo2() {  }
};

Foo2 test(Foo2 byval) { return byval; }

du erhältst:

test(Foo2):
        mov     edx, DWORD PTR [rsi]
        mov     rax, rdi
        mov     DWORD PTR [rdi], edx
        ret

mit nutzlosem Laden und Lagern.


Dieses Argument überzeugt mich nicht. Der nicht triviale Destruktor unternimmt nichts, um die Als-ob-Regel zu verbieten. Wenn die Adresse nicht beachtet wird, gibt es absolut keinen Grund, warum es eine geben muss. Ein konformer Compiler könnte es also gerne in ein Register eintragen, wenn dies das beobachtbare Verhalten nicht ändert (und aktuelle Compiler tun dies tatsächlich, wenn die Aufrufer bekannt sind ).
ComicSansMS

1
Leider ist es umgekehrt (ich stimme zu, dass einiges davon bereits über den Verstand hinausgeht). Um genau zu sein: Ich bin nicht davon überzeugt, dass die von Ihnen angegebenen Gründe notwendigerweise einen denkbaren ABI machen würden, der es erlaubt, den Strom std::unique_ptrin einem Register nicht konform weiterzuleiten.
ComicSansMS

3
"trivial destructor [CITATION NEEDED]" eindeutig falsch; Wenn kein Code tatsächlich von der Adresse abhängt, bedeutet as-if, dass die Adresse auf dem tatsächlichen Computer nicht vorhanden sein muss . Die Adresse muss in der abstrakten Maschine vorhanden sein , aber Dinge in der abstrakten Maschine, die keinen Einfluss auf die tatsächliche Maschine haben, sind Dinge, als ob sie beseitigt werden dürfen.
Yakk - Adam Nevraumont

2
@einpoklum Es gibt nichts in dem Standard, dass Zustandsregister existieren. Das Schlüsselwort register gibt nur an, dass Sie die Adresse nicht übernehmen können. Für den Standard gibt es nur eine abstrakte Maschine. "als ob" bedeutet, dass sich jede reale Maschinenimplementierung nur "als ob" sich die abstrakte Maschine bis zu einem vom Standard nicht definierten Verhalten verhalten muss. Jetzt gibt es sehr herausfordernde Probleme, ein Objekt in einem Register zu haben, über die alle ausführlich gesprochen haben. Aufrufkonventionen, die im Standard ebenfalls nicht behandelt werden, haben praktische Anforderungen.
Yakk - Adam Nevraumont

1
@einpoklum Nein, in dieser abstrakten Maschine haben alle Dinge Adressen; Adressen sind jedoch nur unter bestimmten Umständen erkennbar. Das registerSchlüsselwort sollte es für die physische Maschine trivial machen, etwas in einem Register zu speichern, indem Dinge blockiert werden, die es praktisch schwieriger machen, "keine Adresse" in der physischen Maschine zu haben.
Yakk - Adam Nevraumont

2

Ist dies auf einigen Plattformen tatsächlich eine ABI-Anforderung? (welche?) Oder ist es in bestimmten Szenarien nur eine Pessimierung?

Wenn etwas an der Grenze der Komplikationseinheit sichtbar ist, wird es Teil des ABI, unabhängig davon, ob es implizit oder explizit definiert ist.

Warum ist der ABI so?

Das grundlegende Problem besteht darin, dass Register ständig gespeichert und wiederhergestellt werden, wenn Sie den Aufrufstapel nach unten und oben bewegen. Es ist also nicht praktisch, einen Verweis oder Zeiger auf sie zu haben.

In-Lining und die daraus resultierenden Optimierungen sind nett, wenn es passiert, aber ein ABI-Designer kann sich nicht darauf verlassen, dass es passiert. Sie müssen den ABI im schlimmsten Fall entwerfen. Ich glaube nicht, dass Programmierer mit einem Compiler sehr zufrieden wären, bei dem sich der ABI je nach Optimierungsstufe geändert hat.

Ein trivial kopierbarer Typ kann in Registern übergeben werden, da die logische Kopieroperation in zwei Teile aufgeteilt werden kann. Die Parameter werden in die Register kopiert, die vom Aufrufer zum Übergeben von Parametern verwendet werden, und dann vom Angerufenen in die lokale Variable kopiert. Ob die lokale Variable einen Speicherort hat oder nicht, ist daher nur Sache des Angerufenen.

Bei einem Typ, bei dem ein Kopier- oder Verschiebungskonstruktor verwendet werden muss, kann der Kopiervorgang nicht auf diese Weise aufgeteilt werden, sodass er im Speicher übergeben werden muss.

Hat das C ++ - Standardkomitee diesen Punkt in den letzten Jahren oder jemals diskutiert?

Ich habe keine Ahnung, ob die Normungsgremien dies berücksichtigt haben.

Die naheliegende Lösung für mich wäre, der Sprache korrekte destruktive Bewegungen hinzuzufügen (anstelle des aktuellen halben Hauses eines "gültigen, aber ansonsten nicht spezifizierten Zustands") und dann eine Möglichkeit einzuführen, einen Typ als "triviale destruktive Bewegungen" zu kennzeichnen "Auch wenn es keine trivialen Kopien zulässt.

Eine solche Lösung würde jedoch erfordern, dass der ABI des vorhandenen Codes gebrochen wird, um ihn für vorhandene Typen zu implementieren, was einiges an Widerstand mit sich bringen kann (obwohl ABI-Brüche aufgrund neuer C ++ - Standardversionen nicht beispiellos sind, zum Beispiel die Änderungen von std :: string in C ++ 11 führte zu einer ABI-Unterbrechung.


Können Sie näher erläutern, wie durch ordnungsgemäße destruktive Bewegungen ein unique_ptr in einem Register übergeben werden kann? Wäre das so, weil dadurch die Anforderung an adressierbaren Speicher fallengelassen werden könnte?
Einpoklum

Richtige destruktive Bewegungen würden die Einführung eines Konzepts trivialer destruktiver Bewegungen ermöglichen. Dies würde es ermöglichen, dass dieser triviale Schritt vom ABI auf die gleiche Weise aufgeteilt wird, wie es triviale Kopien heute sein können.
Plugwash

Sie möchten jedoch auch eine Regel hinzufügen, nach der ein Compiler eine Parameterübergabe als reguläre Verschiebung oder Kopie implementieren kann, gefolgt von einer "trivialen destruktiven Verschiebung", um sicherzustellen, dass es immer möglich ist, Register zu übergeben, unabhängig davon, woher der Parameter stammt.
Plugwash

Weil die Registergröße einen Zeiger enthalten kann, aber eine unique_ptr-Struktur? Was ist sizeof (unique_ptr <T>)?
Mel Viso Martinez

@MelVisoMartinez Sie sind möglicherweise verwirrend unique_ptrund shared_ptrsemantisch: shared_ptr<T>Sie können dem ctor 1) ein ptr x für das abgeleitete Objekt U bereitstellen, das mit dem statischen Typ U w / dem Ausdruck gelöscht werden soll delete x;(Sie benötigen hier also keinen virtuellen dtor) 2) oder sogar eine benutzerdefinierte Bereinigungsfunktion. Das bedeutet, dass der Laufzeitstatus innerhalb des shared_ptrSteuerblocks verwendet wird, um diese Informationen zu codieren. OTOH unique_ptrverfügt über keine solche Funktionalität und codiert das Löschverhalten im Status nicht. Die einzige Möglichkeit, die Bereinigung anzupassen, besteht darin, eine andere Vorlageninstanz (einen anderen Klassentyp) zu erstellen.
Neugieriger

-1

Zuerst müssen wir zu dem zurückkehren, was es bedeutet, nach Wert und Referenz zu gehen.

Für Sprachen wie Java und SML ist die Übergabe von Werten unkompliziert (und es gibt keine Übergabe als Referenz), genau wie das Kopieren eines Variablenwerts, da alle Variablen nur Skalare sind und eine integrierte Kopiersemantik haben: Sie zählen entweder als Arithmetik Geben Sie C ++ oder "Referenzen" ein (Zeiger mit unterschiedlichem Namen und Syntax).

In C haben wir skalare und benutzerdefinierte Typen:

  • Skalare haben einen numerischen oder abstrakten Wert (Zeiger sind keine Zahlen, sie haben einen abstrakten Wert), der kopiert wird.
  • Bei Aggregattypen werden alle möglicherweise initialisierten Mitglieder kopiert:
    • für Produkttypen (Arrays und Strukturen): Rekursiv werden alle Elemente von Strukturen und Elementen von Arrays kopiert (die C-Funktionssyntax ermöglicht es nicht, Arrays direkt nach Wert zu übergeben, sondern nur Arrays-Mitglieder einer Struktur, aber das ist ein Detail ).
    • für Summentypen (Gewerkschaften): Der Wert des "aktiven Mitglieds" bleibt erhalten; Offensichtlich ist die Kopie von Mitglied zu Mitglied nicht in Ordnung, da nicht alle Mitglieder initialisiert werden können.

In C ++ können benutzerdefinierte Typen eine benutzerdefinierte Kopiersemantik haben, die eine wirklich "objektorientierte" Programmierung mit Objekten ermöglicht, die Eigentümer ihrer Ressourcen sind, und "Deep Copy" -Operationen. In einem solchen Fall ist eine Kopieroperation wirklich ein Aufruf einer Funktion, die fast beliebige Operationen ausführen kann.

Für C-Strukturen, die als C ++ kompiliert wurden, wird "Kopieren" weiterhin als Aufruf der benutzerdefinierten Kopieroperation (entweder Konstruktor oder Zuweisungsoperator) definiert, die implizit vom Compiler generiert werden. Dies bedeutet, dass die Semantik eines gemeinsamen C / C ++ - Teilmengenprogramms in C und C ++ unterschiedlich ist: In C wird ein ganzer Aggregattyp kopiert, in C ++ wird eine implizit generierte Kopierfunktion aufgerufen, um jedes Mitglied zu kopieren. Das Endergebnis ist, dass in jedem Fall jedes Mitglied kopiert wird.

(Ich denke, es gibt eine Ausnahme, wenn eine Struktur innerhalb einer Union kopiert wird.)

Für einen Klassentyp ist die einzige Möglichkeit (außerhalb von Union-Kopien), eine neue Instanz zu erstellen, die Verwendung eines Konstruktors (selbst für solche mit trivial vom Compiler generierten Konstruktoren).

Sie können die Adresse eines r-Werts nicht über einen unären Operator übernehmen &, dies bedeutet jedoch nicht, dass kein r-Wert-Objekt vorhanden ist. und ein Objekt hat per Definition eine Adresse ; und diese Adresse wird sogar durch ein Syntaxkonstrukt dargestellt: Ein Objekt vom Klassentyp kann nur von einem Konstruktor erstellt werden und hat einen thisZeiger; Für triviale Typen gibt es jedoch keinen vom Benutzer geschriebenen Konstruktor, sodass kein Platz zum Platzieren vorhanden istthis erst nach dem Erstellen und Benennen der Kopie ein ist.

Für den Skalartyp ist der Wert eines Objekts der Wert des Objekts, der reine mathematische Wert, der im Objekt gespeichert ist.

Für einen Klassentyp ist der einzige Begriff für einen Wert des Objekts eine andere Kopie des Objekts, die nur von einem Kopierkonstruktor erstellt werden kann, eine echte Funktion (obwohl diese Funktion für triviale Typen so speziell trivial ist, kann dies manchmal sein erstellt, ohne den Konstruktor aufzurufen). Dies bedeutet, dass der Wert des Objekts das Ergebnis einer Änderung des globalen Programmstatus durch eine Ausführung ist . Es greift nicht mathematisch zu.

Das Übergeben von Werten ist also wirklich keine Sache: Es ist das Übergeben eines Kopierkonstruktoraufrufs , was weniger hübsch ist. Es wird erwartet, dass der Kopierkonstruktor eine sinnvolle "Kopier" -Operation gemäß der richtigen Semantik des Objekttyps ausführt, wobei seine internen Invarianten (abstrakte Benutzereigenschaften, keine intrinsischen C ++ - Eigenschaften) berücksichtigt werden.

Wertübergabe eines Klassenobjekts bedeutet:

  • Erstellen Sie eine andere Instanz
  • Lassen Sie dann die aufgerufene Funktion auf diese Instanz wirken.

Beachten Sie, dass das Problem nichts damit zu tun hat, ob die Kopie selbst ein Objekt mit einer Adresse ist: Alle Funktionsparameter sind Objekte und haben eine Adresse (auf sprachensemantischer Ebene).

Die Frage ist, ob:

  • Die Kopie ist ein neues Objekt, das wie bei Skalaren mit dem reinen mathematischen Wert (wahrer reiner Wert) des ursprünglichen Objekts initialisiert wurde .
  • oder die Kopie ist der Wert des Originalobjekts , wie bei Klassen.

Im Fall eines trivialen Klassentyps können Sie weiterhin das Mitglied der Mitgliedskopie des Originals definieren, sodass Sie aufgrund der Trivialität der Kopiervorgänge (Kopierkonstruktor und Zuweisung) den reinen Wert des Originals definieren können. Nicht so bei beliebigen speziellen Benutzerfunktionen: Ein Wert des Originals muss eine konstruierte Kopie sein.

Klassenobjekte müssen vom Aufrufer erstellt werden. Ein Konstruktor hat formal einen thisZeiger, aber der Formalismus ist hier nicht relevant: Alle Objekte haben formal eine Adresse, aber nur diejenigen, deren Adresse tatsächlich nicht rein lokal verwendet wird (im Gegensatz zu einer *&i = 1;rein lokalen Verwendung der Adresse), müssen genau definiert sein Adresse.

Ein Objekt muss unbedingt an Adresse übergeben werden, wenn es in beiden separat kompilierten Funktionen eine Adresse zu haben scheint:

void callee(int &i) {
  something(&i);
}

void caller() {
  int i;
  callee(i);
  something(&i);
}

Selbst wenn something(address)es sich um eine reine Funktion oder ein Makro handelt oder was auch immer (wie printf("%p",arg)), das die Adresse nicht speichern oder nicht mit einer anderen Entität kommunizieren kann, müssen wir die Adresse übergeben, da die Adresse für ein eindeutiges Objekt intmit einer eindeutigen Adresse genau definiert sein muss Identität.

Wir wissen nicht, ob eine externe Funktion in Bezug auf die an sie übergebenen Adressen "rein" ist.

Hier ist das Potenzial für eine echte Verwendung der Adresse in einem nicht trivialen Konstruktor oder Destruktor auf der Anruferseite wahrscheinlich der Grund, den sicheren, vereinfachten Weg zu gehen und dem Objekt eine Identität im Anrufer zu geben und seine Adresse so zu übergeben, wie sie es macht Stellen Sie sicher, dass jede nicht triviale Verwendung der Adresse im Konstruktor nach der Konstruktion und im Destruktor konsistent ist : Sie thismuss über die Objektexistenz hinweg gleich erscheinen.

Ein nicht trivialer Konstruktor oder Destruktor wie jede andere Funktion kann den thisZeiger auf eine Weise verwenden, die Konsistenz über seinen Wert erfordert, obwohl einige Objekte mit nicht trivialem Material möglicherweise nicht:

struct file_handler { // don't use that class!
    file_handler () { this->fileno = -1; }
    file_handler (int f) { this->fileno = f; }
    file_handler (const file_handler& rhs) {
        if (this->fileno != -1)
            this->fileno = dup(rhs.fileno);
        else
            this->fileno = -1;
    }
    ~file_handler () {
        if (this->fileno != -1)
            close(this->fileno); 
    }
    file_handler &operator= (const file_handler& rhs);
};

Beachten Sie, dass in diesem Fall this->die Objektidentität trotz expliziter Verwendung eines Zeigers (explizite Syntax ) irrelevant ist: Der Compiler könnte das Objekt durchaus bitweise kopieren, um es zu verschieben und "Elision kopieren". Dies basiert auf dem Grad der "Reinheit" der Verwendung thisin speziellen Mitgliedsfunktionen (Adresse entweicht nicht).

Aber Reinheit ist nicht verfügbar Attribut auf der Standard - Erklärung Ebene (Compiler - Erweiterungen vorhanden ist, dass die Add Reinheit Beschreibung auf nicht Inline - Funktionsdeklaration), so Sie kein ABI basierend auf Reinheit des Codes definieren, die nicht zur Verfügung stehen (Code kann oder möglicherweise nicht inline und für die Analyse verfügbar).

Reinheit wird als "sicherlich rein" oder "unrein oder unbekannt" gemessen. Die Gemeinsamkeit oder Obergrenze der Semantik (tatsächlich maximal) oder LCM (Least Common Multiple) ist "unbekannt". Der ABI entscheidet sich also für Unbekanntes.

Zusammenfassung:

  • Bei einigen Konstrukten muss der Compiler die Objektidentität definieren.
  • Der ABI wird anhand von Programmklassen und nicht anhand spezifischer Fälle definiert, die möglicherweise optimiert werden.

Mögliche zukünftige Arbeit:

Ist die Reinheitsanmerkung nützlich genug, um verallgemeinert und standardisiert zu werden?


1
Ihr erstes Beispiel erscheint irreführend. Ich denke, Sie machen nur einen Punkt im Allgemeinen, aber zuerst dachte ich, Sie machen eine Analogie zum Code in der Frage. Nimmt void foo(unique_ptr<int> ptr)aber das Klassenobjekt nach Wert . Dieses Objekt hat ein Zeigerelement, aber wir sprechen über das Klassenobjekt selbst, das als Referenz übergeben wird. (Da es nicht trivial kopierbar ist, benötigt sein Konstruktor / Destruktor eine konsistentethis .) Dies ist das eigentliche Argument und nicht mit dem ersten Beispiel einer expliziten Referenzübergabe verbunden . In diesem Fall wird der Zeiger in einem Register übergeben.
Peter Cordes

@PeterCordes " Sie haben eine Analogie zum Code in der Frage gemacht. " Genau das habe ich getan. " das Klassenobjekt nach Wert " Ja, ich sollte wahrscheinlich erklären, dass es im Allgemeinen keinen "Wert" eines Klassenobjekts gibt, so dass der Wert für einen nicht mathematischen Typ nicht "nach Wert" ist. " Dieses Objekt hat ein Zeigerelement " Die ptr-ähnliche Natur eines "intelligenten ptr" ist irrelevant; und so ist das ptr-Mitglied des "intelligenten ptr". Ein ptr ist nur ein Skalar wie ein int: Ich habe ein "Smart Fileno" -Beispiel geschrieben, das zeigt, dass "Besitz" nichts mit "Tragen eines ptr" zu tun hat.
Neugieriger

1
Der Wert eines Klassenobjekts ist seine Objektdarstellung. Für unique_ptr<T*>, das ist die gleiche Größe und Layout wie T*und paßt in einem Register. Trivial kopierbare Klassenobjekte können wie die meisten Aufrufkonventionen in Registern in x86-64 System V als Wert übergeben werden . Dies macht eine Kopie des unique_ptrObjekts, anders als in Ihrem intBeispiel , wo die Angerufenen &i ist die Adresse des Anrufers ist , iweil Sie als Referenz übergaben an der C ++ Ebene , nicht nur als asm Implementierungsdetail.
Peter Cordes

1
Ähm, Korrektur zu meinem letzten Kommentar. Es geht nicht nur darum, eine Kopie des unique_ptrObjekts zu erstellen. Es wird verwendet, std::moveso dass es sicher ist, es zu kopieren, da dies nicht zu 2 Kopien desselben führt unique_ptr. Bei einem trivial kopierbaren Typ wird jedoch das gesamte Aggregatobjekt kopiert. Wenn dies ein einzelnes Mitglied ist, behandeln gute Aufrufkonventionen es genauso wie einen Skalar dieses Typs.
Peter Cordes

1
Sieht besser aus. Hinweise: Für C-Strukturen, die als C ++ kompiliert wurden - Dies ist keine nützliche Methode, um den Unterschied zwischen C ++ einzuführen. In C ++ struct{}ist eine C ++ - Struktur. Vielleicht sollten Sie "einfache Strukturen" oder "anders als C" sagen. Denn ja, da gibt es einen Unterschied. Wenn Sie atomic_intals Strukturelement verwenden, kopiert C es nicht atomar, C ++ - Fehler im gelöschten Kopierkonstruktor. Ich habe vergessen, was C ++ bei Strukturen mit volatileMitgliedern macht. Mit C können Sie struct tmp = volatile_struct;das Ganze kopieren (nützlich für ein SeqLock). C ++ wird nicht.
Peter Cordes
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.