Referenz auf noch nicht initialisiertes Objekt binden
-
Ich bin mir gerade etwas unsicher, ob das hier mit dem Standard vereinbar ist und wollte mal kurz eure Meinung dazu hören:
#include <iostream> struct Object { int data; }; alignas(Object) std::byte object_storage[sizeof(Object)]; Object& object = *reinterpret_cast<Object*>(object_storage); int main() { new (object_storage) Object{ 42 }; std::cout << object.data << std::endl; }
Ich denke das ist okay, da ich erst dann auf die Referenz zugreife, wenn das Objekt initialisiert wurde. Die Referenz jedoch bereits vorher zu initialisieren macht mich dennoch etwas stutzig.
Kann vielleicht jemand meine Bedenken zerstreuen oder mir zumindest sagen, dass ich hier gerade Blödsinn mache?
Edit: Zumindest der aktuelle Clang hat auch mit allen Warnungen und UB-Sanitizer nichts zu beanstanden, wahrscheinlich bin ich grad nur etwas paranoid : https://godbolt.org/z/dz96cof6a
-
@Finnegan sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Ich denke das ist okay, da ich erst dann auf die Referenz zugreife, wenn das Objekt initialisiert wurde. Die Referenz jedoch bereits vorher zu initialisieren macht mich dennoch etwas stutzig.
Du initialisierst hier nicht, Du machst eine Konstruktion eines Objektes (placement new) in einem Array, was das passende Alignment hat. Daran ist nichts auszusetzen.
Die Referenz wird schon vorher an den Speicher gebunden, und danach erst das Objekt konstruiert. Auch das geht, nur muss man aufpassen, dass das Objekt auch „lebt“, wenn man darauf zugreift. Sonst ist es UB.
-
@Finnegan sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Die Referenz jedoch bereits vorher zu initialisieren macht mich dennoch etwas stutzig.
Das ist aber der korrekte Weg ...
Fehlerursache
In C++ ist es nicht erlaubt, eine Referenz auf ein Objekt zu erstellen, das noch nicht initialisiert wurde. Wenn du versuchst, eine Referenz auf ein solches uninitialisiertes Objekt zu binden, wird ein Laufzeitfehler auftreten oder das Programm wird undefiniertes Verhalten zeigen.
Beispiel
Hier ein einfaches Beispiel:
#include <iostream> class MyClass { public: void display() { std::cout << "Hello, world!" << std::endl; } }; int main() { MyClass& ref; // Fehler: Referenz auf uninitialisiertes Objekt ref.display(); // Diese Zeile wird niemals erreicht return 0; }
Lösung
Um dieses Problem zu beheben, musst du sicherstellen, dass die Referenz auf ein korrekt initialisiertes Objekt zeigt. Zum Beispiel:
int main() { MyClass obj; // Initialisierung des Objekts MyClass& ref = obj; // Jetzt wird die Referenz korrekt initialisiert ref.display(); // Das funktioniert jetzt return 0; }
Fazit
Stelle sicher, dass jede Referenz, die du verwendest, auf ein initialisiertes und valides Objekt zeigt. Überprüfung der Initialisierung vor der Verwendung von Referenzen ist entscheidend, um Laufzeitfehler zu vermeiden.
Weitere Beispiele
class MyClass { public: void display() { std::cout << "Hello, world!" << std::endl; } }; int main() { MyClass obj; // Das Objekt wird hier initialisiert MyClass& ref = obj; // Referenz initialisiert mit obj ref.display(); // Sicherer Zugriff auf ein valides Objekt return 0; }
void processObject(MyClass& obj) { // obj muss ein initialisiertes und valides Objekt sein obj.display(); } int main() { MyClass obj; // Initialisierung des Objekts processObject(obj); // Übergebe das Objekt an die Funktion return 0; }
void processObject(MyClass* obj) { if (obj) { // Überprüfen, ob der Zeiger nicht null ist obj->display(); } else { std::cout << "Das Objekt ist nicht gültig!" << std::endl; } } int main() { MyClass* obj = nullptr; // Zeiger initialisieren // Wenn du sicher bist, dass ein objekt existiert MyClass validObj; obj = &validObj; // Zuweisen eines gültigen Objekts processObject(obj); // Übergibt das Objekt return 0; }
#include <iostream> #include <optional> class MyClass { public: void display() { std::cout << "Hello, world!" << std::endl; } }; void processObject(std::optional<MyClass>& objOpt) { if (objOpt) { // Überprüfen, ob ein Objekt vorhanden ist objOpt->display(); // Arrow-Operator für Zugriff } else { std::cout << "Das Objekt ist nicht gültig!" << std::endl; } } int main() { std::optional<MyClass> obj; // Initialisierung ohne Objekt // Initialisiere und verwende das Objekt obj.emplace(); // Erstellen des Objekts processObject(obj); return 0; }
hth
-
@john-0 sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Du machst eine Konstruktion eines Objektes (placement new)
Das schimpft sich ja gerade Initialisierung. Also Deklaration und Erstzuweisung in einem Schritt ...
-
@Finnegan sagte in Referenz auf noch nicht initialisiertes Objekt binden:
alignas(Object) std::byte object_storage[sizeof(Object)];
Object& object = reinterpret_cast<Object>(object_storage);Ich sehe hier auf keine Bedenken. Ich gestehe aber, dass ich hier an meine C Projekte denke:
#pragma pack(1) struct Object { int data; }; #pragma pack() static const size_t ObjSize = sizeof(Object); uint8_t object_storage[ObjSize]; Object* object = (Object*)object_storage;
In meinem Fall ist
object_storage
immer serialisierte Daten undObject
eine mögliche Interpretation.
-
@Finnegan Das ist schon allein deshalb nicht problematisch, weil Referenzen ja auch auf bspw. globale/externe Variablen verweisen koennen, die aufgrund der Initialisierungsreihenfolge erst nach der Referenz initialisiert werden.
Siehe auch https://eel.is/c++draft/dcl.ref#6, wo dein Beispiel auch quasi als wohldefiniert impliziert wird.
-
@john-0 sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Du initialisierst hier nicht, Du machst eine Konstruktion eines Objektes (placement new) in einem Array, was das passende Alignment hat. Daran ist nichts auszusetzen.
Oh, ist "Konstruktion" nicht auch eine Form von "Initialisierung", oder braucht hier mein Begriffsverständnis ein Update?
@Quiche-Lorraine sagte in Referenz auf noch nicht initialisiertes Objekt binden:
In meinem Fall ist
object_storage
immer serialisierte Daten undObject
eine mögliche Interpretation.Ja, in C hätte ich mit so einem Konstrukt auch keinerlei Bedenken gehabt. Mir gings hier vor allem darum, dass der Compiler nicht z.B. denkt, dass er die Referenz so "optimieren" kann, dass er sie gar nicht erst initialisiert, weil die ja ohnehin auf kein Objekt verweist. "UB kann nicht auftreten"-Optimierungen können ätzend zu debuggen sein, wenn man tatsächlich irgendwo UB hat.
@Columbo sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Siehe auch https://eel.is/c++draft/dcl.ref#6, wo dein Beispiel auch quasi als wohldefiniert impliziert wird.
Ja danke. Da hab ich mich gestern schwer getan das zu finden. "The object designated by such a glvalue can be outside its lifetime" in Note 2 ist nach meinem Verständnis eine Aussage, die das explizit erlaubt.
Das erinnert mich auch daran, dass ich auf den Sonderfall des "null pointer" achten muss. Der betreffende Code ist in einem x86-Baremetal-Hobbyprojekt, wo mein Linker-Skript tatsächlich Daten so platzieren kann, dass sie im geladenen Programm an
ds:0x0
liegen. Da brauch ich noch ein Padding, wenn ich keine bösen Ohrfeigen vom Compiler bekommen will -const char* hello = "Hello World";
und(hello == nullptr)
ist bestimmt nicht gesund.
-
@Finnegan sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Mir gings hier vor allem darum, dass der Compiler nicht z.B. denkt, dass er die Referenz so "optimieren" kann, dass er sie gar nicht erst initialisiert, weil die ja ohnehin auf kein Objekt verweist.
Auch Compileroptimierungen nehmen eine Variable oder Referenz nicht einfach so raus, solange diese noch im Gültigkeitsbereich ist und gelesen wird.
-
@noLust Compiler Optimierungen dürfen und machen alles, so lange es das vom Nutzer beobachtbare Verhalten nicht ändert. Es gibt da lustige Fälle, in denen zum Beispiel ein überschreiben des Speichers weg optimiert wird, wenn der Speicherbereich hinterher nicht mehr benutzt wird, was man aber unter Umständen explizit haben möchte, z.B. in kryptographischen Anwendungen.
-
@Finnegan sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Mir gings hier vor allem darum, dass der Compiler nicht z.B. denkt, dass er die Referenz so "optimieren" kann, dass er sie gar nicht erst initialisiert, weil die ja ohnehin auf kein Objekt verweist.
Ich verstehe diesen Satz nicht.
Kein Objekt? Du legst doch die globale Variable
object_storage
an. Und diese wird vom Compiler zero-initialized.https://en.cppreference.com/w/cpp/language/zero_initialization
Danach legst du eine Referenz namens
object
an, welche aufobject_storage
verweist. Der cast ist ein wenig verwirrend, aber ok.Und danach machst du ein placement new. Du könntest aber hier genauso
object.data = 42;
schreiben."UB kann nicht auftreten"-Optimierungen können ätzend zu debuggen sein, wenn man tatsächlich irgendwo UB hat.
Kenne ich. Ich hatte vor kurzem dank "The Old New Thing" ein WinAPI Rätsel zum Thema Subclassing gelöst, wo nur Dr. Memory motzte.
-
@Finnegan sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Oh, ist "Konstruktion" nicht auch eine Form von "Initialisierung", oder braucht hier mein Begriffsverständnis ein Update?
Dein Begriffsverständnis braucht ein Update. Ein C++ Objekt durchläuft zwei Phasen beim Erzeugen:
- Die Konstruktion: Dabei wird das Objekt erzeugt, und man darf danach mit den Mittel von C++ darauf zu greifen. (POD-Typen brauchen keine Konstruktion.)
- Die Initialisierung: Dabei wird das Objekt in einen programmlogischen wohldefinierten Zustand gebracht.
Die Designphilosophie RAII versucht gerade wann immer möglich diese beiden Phasen zusammen zu legen. Seit C++11 ist es nun noch sehr selten notwendig vom RAII Gedanken abzuweichen und z.B. separate init Funktionen zu schreiben. Früher gab es das häufiger.
In Deinem Fall ist das nun so, dass nach dem placement new über die Referenz auf das Objekt zugegriffen werden darf, da es konstruiert wurde. Dieser Aspekt ist hier wichtig. Bei nonPOD Objekten darf der Compiler nämlich noch Compilermagie betreiben und Weiteres als die offensichtlichen Member per Konstruktor initialisieren. Bei einem POD-Typen würde ein simpler Cast ausreichen, damit darauf zugegriffen werden kann.
-
@Quiche-Lorraine sagte in Referenz auf noch nicht initialisiertes Objekt binden:
@Finnegan sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Mir gings hier vor allem darum, dass der Compiler nicht z.B. denkt, dass er die Referenz so "optimieren" kann, dass er sie gar nicht erst initialisiert, weil die ja ohnehin auf kein Objekt verweist.
Ich verstehe diesen Satz nicht.
Kein Objekt? Du legst doch die globale Variable
object_storage
an. Und diese wird vom Compiler zero-initialized.Sorry, der Satz ist auch etwas lang geworden. Meine Befürchtung war: Compiler sieht, da ist kein
Object
, sondern nur einbyte[sizeof(Object)]
. Wenn das UB wäre, hätte die z.B. die Initialisierung der Referenz kommentarlos rausfliegen können.
-
@john-0 sagte in Referenz auf noch nicht initialisiertes Objekt binden:
@Finnegan sagte in Referenz auf noch nicht initialisiertes Objekt binden:
Oh, ist "Konstruktion" nicht auch eine Form von "Initialisierung", oder braucht hier mein Begriffsverständnis ein Update?
Dein Begriffsverständnis braucht ein Update. Ein C++ Objekt durchläuft zwei Phasen beim Erzeugen:
- Die Konstruktion: Dabei wird das Objekt erzeugt, und man darf danach mit den Mittel von C++ darauf zu greifen. (POD-Typen brauchen keine Konstruktion.)
- Die Initialisierung: Dabei wird das Objekt in einen programmlogischen wohldefinierten Zustand gebracht.
Die Designphilosophie RAII versucht gerade wann immer möglich diese beiden Phasen zusammen zu legen. Seit C++11 ist es nun noch sehr selten notwendig vom RAII Gedanken abzuweichen und z.B. separate init Funktionen zu schreiben. Früher gab es das häufiger.
Okay. So wie ich das verstehe:
std::ofstream out; out.write("ABC", 3);
Hier würdest du
out
als konstruiert, aber nicht initialisiert bezeichnen?So wie ich die Begriffe bisher verwendet habe ist
out
beides. Interne Datenstrukturen wie File Handle wurden im Konstruktor mit irgendwelchen Werten initialisiert, verweisen aber auf keine Datei. Ich hätte hier eher von fehlerhafter Initialisierung gesprochen.Aber ich denke ich verstehe, worauf du hinaus willst: Ein
int value;
ist konstruiert, aber nicht initialisiert, während einint value = 0;
initialisiert ist. Nach meinem bisherigen Verständnis fällt es mir bei Objekten mit Konstruktor aber schwer, Konstruktion von Initialisierung überhaupt zu trennen, da auch einObject object;
via Default-Konstruktor initialisiert wird (Default Initialization). Selbst wenn es nur ein explizit leerer oder der compiler-generierte Default-Konstruktor ist.Daher bin ich bei dem Argument mit dem RAII auch noch skeptisch. Wenn ich mir den verlinkten cppreference-Artikel ansehe, dann wird da der Begriff schon etwas anders verwendet. Auch der C++-Standard nennt das so wie ich das sehe auch "initialization". Der erwähnt zwar auch Fälle wo keine Initialisierung stattfindet, aber die sind lediglich für primitive Datentypen oder Referenzen/Pointer und sowas. Sobald ein anwendbarer Konstruktor da ist, findet laut Standard "Initialisierung" statt, indem dieser aufgerufen wird.
Ich weiß aber, was du meinst - für dich ist "Initialisierung" von der Programmlogik abhängig (
write
ohneopen
ist ein Bug). Da macht das dann mit dem RAII auch Sinn. Ich glaube aber dennoch, das ist zumindest aus Perspektive des Standards nicht ganz korrekt - auch wenn ich deine Definition für den praktischen Einsatz durchaus als sinnvoll erachte.
-
@Finnegan Keine Ahnung was dieser Kasper da schon wieder faselt. Ich glaube er hat den Begriff "Allokation" mit "Konstruktion" verwechselt oder so. Um sich wichtig zu machen. Keine Ahnung.
Es gibt nur eine solche Phase im Bezug auf die Lebenszeit eines Objekts, und das ist die Initialisierung (die im Standardjargon weitgehend synonym zu Konstruktion ist). Dass Initialisierung eben manchmal ausbleibt, also ein Objekt vacuous initialized ist, ist zwar terminologisch ungluecklich, aber hat nichts mit 'construction' zu tun (was ein Begriff ist, der sich auf die Phase der Ausfuehrung aller Konstruktoren, member initializer etc. eines Klassenobjekts bezieht).
Ein int value; ist konstruiert, aber nicht initialisiert,
Formell: ein
int value
ist default-initialized, was also keine eigentliche Initialisierung ist (weil nix passiert), was ergo vacuous initialization ist. Was aber insbesondere wichtig ist, ist die Tatsache, dass das Objekt am leben ist, und dass ein Zugriff wohldefiniert (auch wenn fehlerhaft) ist.Es ist aber sowohl umgangssprachlich als auch normativ korrekt zu sagen:
i
ist nicht initialisiert.Da macht das dann mit dem RAII auch Sinn. Ich glaube aber dennoch, das ist zumindest aus Perspektive des Standards nicht ganz korrekt - auch wenn ich deine Definition für den praktischen Einsatz durchaus als sinnvoll erachte.
Der Standard redet einerseits von Initialization, und andererseits von bestimmten definierten Begriffen wie "vacuous initialization" und "default-initialization" etc. der Begriff "Initialisierung" an sich ueberlappt sich durchaus mit dem gemeinen Gebrauch.
Die Designphilosophie RAII versucht gerade wann immer möglich diese beiden Phasen zusammen zu legen.
Bist jetzt auch noch Philosoph, ey?