Problematik von reinterpret_cast
-
Da ich im anderen Beitrag dazu keine Antwort mehr erhalten habe und die Thematik einen eigenen Thread verdient hat, hielt ich es für angebracht, einen neuen zu eröffnen.
Damals wurde ich auf Folgendes hingewiesen:
camper schrieb:
reinterpret_cast interpretiert niemals irgendwelche Speicherbereiche - daher kann reinterpret_cast (als Zeiger- oder Referenzcast) auch nie fehlschlagen, solange die Alignmentvoraussetzungen (5.2.10/7) erfüllt sind (das ist unproblematisch bei vector<char>, da dieser die globale Allokationsfunktion operator new benutzt, die stets hinreichend ausgerichteten Speicher liefert). Problematisch ist der folgende Zugriff auf den Speicher, dieser ist bis auf wenige Ausnahmen (3.10/15) immer undefiniert - das es oft genug trotzdem funktioniert ist dummen Compilern zu verdanken, denn 3.10/15 ist primär eine Optimierungsregel (in Analogie zum Schlüsselwort restrict in C, wärend sich restrict um Zugriffspfade auf Objekte gleichen Typs kümmert, geht es bei 3.10/15 um Zugriffe auf Objekte mit einem anderen Typ). Eine ausfürlichere Erklärung plus den Code gibt es später.
Nun würde mich natürlich interessieren, was
reinterpret_cast
tut, wenn nicht Speicherbereiche (Bitmuster) neu zu interpretieren. Und worin die von camper genannte Problematik besteht...
-
mich auch.
-
hallo
mich auch...
-
reinterpret_cast selbst macht nichts. Es sorgt nur dafür, dass ein Zeiger in jeden beliebigen anderen Zeiger umgewandelt werden kann.
D.h. der ursprüngliche Zeiger Speicheradresse wird einfach anders interpretiert.
D.h. nicht, dass ein nachfolgender Zugriff über diesen Zeiger möglich ist...Ich verstehe nicht ganz was camper eigentlich aussagen wollte...
-
Wahrscheinlich meinte er nachfolgend im Sinne von konsekutiv. Die Sache mit dem Alignment lässt zumindest darauf schließen.
-
Martin Richter hat's ja schon gesagt, was reinterpret_cast macht. Den Zeiger/die Referenz von T nach U wandeln, egal in welcher Beziehung T und U stehen. Es greift nicht (!) auf den referenzierten Speicher zu sondern stellt nur die Möglichkeit dazu bereit (das ist der folgende Zugriff).
Dieser folgende Zugriff ist halt in vielen Situationen einfach undefiniert, was zum einen an 3.10/15 liegt und zum anderen an den nicht notwendiger Weise erfüllten Alignment-Anforderungen von U.
-
reinterpret_cast geht allerdings nicht nur mit Zeigern und Referenzen.
möglich (und durchaus schon in Code gesehen worden) ist z.B. folgendesint i; char c[4]; void* p; i = 314562; c = reinterpret_cast<char>(i); p = reinterpret_cast<void*>(i);
-
das heißt also, dass das ergebnis von
char* charptr = new char[4]; int* intptr = reinterpret_cast<int*>(charptr); *intptr = 0;
undefiniert ist?
und wie sieht das dann bei c-style casts aus?int* intptr = (int*)charptr; *intptr = 0; // auch undefiniert?
-
victor schrieb:
das heißt also, dass das ergebnis von
char* charptr = new char[4]; int* intptr = reinterpret_cast<int*>(charptr); *intptr = 0;
undefiniert ist?
und wie sieht das dann bei c-style casts aus?int* intptr = (int*)charptr; *intptr = 0; // auch undefiniert?
Na ich hoffe doch nicht! So etwas habe ich erst vor kurzem selbst verwendet...
Eigentlich mache ich mir da aber keine Sorgen.
-
victor schrieb:
das heißt also, dass das ergebnis von
char* charptr = new char[4]; int* intptr = reinterpret_cast<int*>(charptr); *intptr = 0;
undefiniert ist?
Undefiniert ist es soweit ich weiss nicht. Dein Problem wird nur sein, dass das Ergebnis nicht immer dasselbe sein wird. Auf einer Little- und Bigendian Maschine wirst du jeweils ein anderes Resultat erhalten! Und das zeigt wohl auch schön auf, dass
reinterpret_cast
überhaupt nichts interpretiert, sondern einfach nur den Typzugriff ändert. Kann sehr gefährlich seinGrüssli
-
jup, das ist undefiniert.
auf den meisten 32 Bit-Systemen wird dir das gegeben, was du erwartest, aber auf 16 Bit-Systemem könnte sizeof(int) == 2 sein, dann steht in dem char-Array was anderes - oder sizeof(int) == 10 (unüblich, aber nicht verboten im Standard) und du schreibst die letzten 6 Byte deines int ins Nirvana...
Alignment kann auch noch eine Rolle spielen, usw.bei C-casts ists das selbe, die sind weder typsicher noch sonstwas. Allerdings gibts in C++ ja noch die Kapselung, die bei reinterpret_cast einfach niedergerissen wird. Dinge wie vtables/vptr usw., die vom C++ compiler hinter den Kulissen geregelt werden, werden durch unsachgemäßes und unvorsichtiges Pointergefrickel gern mal rücksichtslos plattgebügelt, und alles was man dann hoffen kann ist dass das Betriebssystem die Prozessleiche dann vollständig entsorgt
-
pumuckl schrieb:
bei C-casts ists das selbe, die sind weder typsicher noch sonstwas.
..., die allerdings meistens dasselbe tun wie ein gewöhnlicher static_cast und von daher bei down- & upcasts genauso sicher wie static_cast sind.
-
@pumuckl,
Aber undefiniert ist dieser Code doch nicht? Es ist genau definiert, wasreinterpret_cast
macht und auch die Zuweisung ist genau definiert. Das Ergebnis ist einfach unter Umständen nicht das, was man erwartet und kann unter gewissen Umständen zu einem undefiniertem Verhalten führen. Aber deshalb würde ich jetzt irgendwie nicht den ganzen Code als undefiniert ansehen ...Grüssli
-
queer_boy schrieb:
pumuckl schrieb:
bei C-casts ists das selbe, die sind weder typsicher noch sonstwas.
..., die allerdings meistens dasselbe tun wie ein gewöhnlicher static_cast und von daher bei down- & upcasts genauso sicher wie static_cast sind.
Naja, ein C-Cast kann alles das auf einmal, was static_cast, reinierpret_cast und const_cast jeweils spezialisiert und teilweise mit Typprüfung können.
-
definiere "alles auf einmal".
die reihenfolge, also wann welcher cast angewendet werden soll, ist klar definiert.das wirklich große manko IMO ist, dass ein C-cast static_cast (bzw. manchmal auch reinterpret_cast) mit const_cast mischen kann. ansonsten ist ein einfaches
int foo (double d) { return (int) d; }
völlig in ordnung (und in diesem fall nicht einmal aus lesbarkeitsgründen problematisch).
-
btw. vielleicht sollten sich alle noch einmal camper im original durchlesen, zur erinnerung ein ausschnitt:
camper schrieb:
... Problematisch ist der folgende Zugriff auf den Speicher, dieser ist bis auf wenige Ausnahmen (3.10/15) immer undefiniert ...
camper hat das zwar nur in klammern, aber um verwirrung zu vermeiden, sollte man das betonen: er spricht nur von casts von zeigern bzw. referenzen (deshalb auch der hinweis auf 5.2.10/7)
schon jemand 3.10/15 durchgelesen?
3.10/15 schrieb:
If a program attempts to access the stored value of an object through an lvalue of other than one of the following types the behavior is undefined:
— the dynamic type of the object,
— a cv-qualified version of the dynamic type of the object,
— a type that is the signed or unsigned type corresponding to the dynamic type of the object,
— a type that is the signed or unsigned type corresponding to a cv-qualified version of the dynamic type of
the object,
— an aggregate or union type that includes one of the aforementioned types among its members (includ-
ing, recursively, a member of a subaggregate or contained union),
— a type that is a (possibly cv-qualified) base class type of the dynamic type of the object,
— a char or unsigned char type.zur verwendung von reinterpret_cast im sinne von 5.2.10/7 lässt sich eigentlich nicht mehr sagen. und die anderen arten, diesen cast zu verwenden, sind sowieso schon implementationsabhängig. das einzige, was noch interessant sein könnte, ist 5.2.10/10.
-
victor schrieb:
das heißt also, dass das ergebnis von
char* charptr = new char[4]; int* intptr = reinterpret_cast<int*>(charptr); *intptr = 0;
undefiniert ist?
und wie sieht das dann bei c-style casts aus?int* intptr = (int*)charptr; *intptr = 0; // auch undefiniert?
Selbstverständlich ist das undefiniert.
Man weiß außerdem nicht, ob der Zeiger korrekt ausgerichtet ist für einen Zugriff als int. Auf einer 32-Bit-Plattform muss der Zeiger in der Regel ein vielfaches von 4 sein, damit der Prozessor nicht mit einem unaligned access aussteigt, auch wenn manche Compiler den Code vielleicht "netterweise" synthetisieren, der einen Speicherwort-übergreifenden Zugriff macht. Das sind aber alles Hintergrundinformationen, mit denen man sich nicht rumschlagen sollte. Das hat im übrigen auch nichts mit Casts zu tun, bzw. nur in sofern, dass sie die Möglichkeit für so einen "bad-aligned" Pointer eröffnen.
Für einen sauberen Wert-Transfer lässt mich ein memcpy benutzen:
int i = 0; memcpy( &i, charptr, sizeof(int));
Dadurch ist sichergestellt, dass i korrekt ausgerichtet ist. Der tatsächliche Inhalt ist aber immernoch undefiniert bzw. zumindest implementierungs- und plattformabhängig.
Um es nochmal zu verdeutlichen, Beispiel:
Speicheradresse Daten 0xa0000000 [000000XX] 0xa0000004 [XXXXXX00]
Hier wäre ein Zeiger auf 0xa0000003 unaligned, wenn man die 4 Byte (XX) lesen will. Der Prozessor müsste zwei Lesezugriffe auf den Speicher machen. Dieser Zeiger ist unaligned. Das soll nicht heißen, dass man jetzt seine Zeiger mit %4 prüft oder sonstwas!
Es soll nur die Problematik ansich verdeutlichen, weil das ganze sonst vielleicht etwas zu abstrakt klingt.
Zu dem hier:
queer_boy schrieb:
pumuckl schrieb:
bei C-casts ists das selbe, die sind weder typsicher noch sonstwas.
..., die allerdings meistens dasselbe tun wie ein gewöhnlicher static_cast und von daher bei down- & upcasts genauso sicher wie static_cast sind.
*hust* Nein, nein und nochmals nein.
C-Casts casten wie ein reinterpret_cast, wenn die Typinformationen unvollständig sind (z.B. nur Forward-Deklarationen). Es gibt keine Warnung oder sonstwas. D.h. wenn ausversehen nur eine Forward-Deklaration auf eine Klasse bekannt ist, wird der C-Cast trotzdem casten und den Pointer zerschießen, während static_cast mit einem Fehler aussteigt.
Das trifft sowohl Up-/Down-/Cross-Casts zu. Insbesondere der erste Fall (Up-Cast) ist subtil, aber trotzdem gefährlich. Insbesondere wenn man sogar in "Lehrbüchern" liest, der Zeiger würde bei einem Up-Cast immer gleich bleiben bei Einfachvererbung.
-
7H3 N4C3R schrieb:
C-Casts casten wie ein reinterpret_cast, wenn die Typinformationen unvollständig sind (z.B. nur Forward-Deklarationen). Es gibt keine Warnung oder sonstwas. D.h. wenn ausversehen nur eine Forward-Deklaration auf eine Klasse bekannt ist, wird der C-Cast trotzdem casten und den Pointer zerschießen, während static_cast mit einem Fehler aussteigt.
Das trifft sowohl Up-/Down-/Cross-Casts zu. Insbesondere der erste Fall (Up-Cast) ist subtil, aber trotzdem gefährlich.das zauberwort ist *meistens*. worauf ich mich bezogen habe, war die aussage, dass C-casts immer gefährlich sind, und zwar deshalb:
bei C-casts ists das selbe, die sind weder typsicher noch sonstwas
es ist genau definiert, wann c-casts was tun. das man sich damit ein bein wegschießen *kann*, ist klar.
7H3 N4C3R schrieb:
Insbesondere wenn man sogar in "Lehrbüchern" liest, der Zeiger würde bei einem Up-Cast immer gleich bleiben bei Einfachvererbung.
darüber habe ich mich kurz erst mit einem prof an der uni gestritten; am ende musste er mir zustimmen
wie schnell solche themen doch wiederkommen...
-
queer_boy schrieb:
das zauberwort ist *meistens*. worauf ich mich bezogen habe, war die aussage, dass C-casts immer gefährlich sind, und zwar deshalb:
bei C-casts ists das selbe, die sind weder typsicher noch sonstwas
es ist genau definiert, wann c-casts was tun. das man sich damit ein bein wegschießen *kann*, ist klar.
Schon klar, das hatte ich auch nicht übersehen.
Aber da wir nunmal alle nur Menschen sind, Fehler machen und Dinge übersehen, sind C-Casts ein Spiel mit dem Feuer. Gerade in komplexeren Quellcodes, wo vieleicht noch Templates beteiligt sind, die die verarbeiteten Typen noch nicht mal kennen, kann schnell mal ein unvollständiger Typ reinrutschen. Warum sollte man dann noch einen C-Cast nehmen, wenn man mit C++-Casts immer auf der sicheren Seite ist?
-
7H3 N4C3R schrieb:
Warum sollte man dann noch einen C-Cast nehmen, wenn man mit C++-Casts immer auf der sicheren Seite ist?
Ich denke, wenn man das macht, dann ist das pure (und u.U. gefährliche) Bequemlichkeit.
Btw. finde ich die C++-Casts auch deutlich besser lesbar, da sie einem beim Überfliegen von Codestücken direkt ins Auge fallen.
-
ich gebe euch ja wohl recht; in letzter zeit musste ich allerdings - gezwungenermaßen - sehr viel mit casts arbeiten (uni
) - und bevor ich dreimal in einer zeile mit static_cast von double nach int castet, nehme ich lieber den cast in funktionsnotation (oder - je nachdem - den c-cast); und zwar genau wegen der lesbarkeit (lustigerweise).
sobald vererbung im spiel ist und man castet - selbst wenn es "nur" ein static_cast ist (in dem zusammenhang wahrscheinlich sogar noch als downcast), sollte man sich imo ein bisschen mehr gedanken darüber machen als bloß "geht nicht, dann halt casten".