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 this
Zeiger; 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 this
Zeiger, 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 int
mit 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 this
muss über die Objektexistenz hinweg gleich erscheinen.
Ein nicht trivialer Konstruktor oder Destruktor wie jede andere Funktion kann den this
Zeiger 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 this
in 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?