"Pointerfrickeleien" - Ist das undefiniert?
-
Context* context; Get((OVERLAPPED**)&context);Das ist laut Standard nicht OK.
Dass es mit den meisten Implementierungen trotzdem funktionieren wird, ist eine andere Sache. Es gibt auch einige APIs die sich darauf verlassen dass soetwas funktioniert, z.B. die COM Funktionen unter Windows (CoCreateInstance, IUnknown::QueryInterface uvm.). Nur deswegen muss man es ja nicht nachmachen.Warum es nicht OK ist: Zeiger auf unterschiedliche Typen können unterschiedlich aussehen.
Stell dir eine C++ Implementierung für eine 32 Bit CPU vor, die nur 32 Bit Worte einzeln adressieren kann. (BTW: So eine CPU kann dann, trotz beschränkung auf 32 Bit, 16 BG RAM adressieren.)
Wie soll jetzt ein Zeiger auf ein "char" aussehen? Nun, laut C++ Standard ist es OK wenn ein "char" 32 Bit hat, also könnte eine C++ Implementierung einfach definieren: char, short und int haben alle 32 Bit - und die Sache wäre geritzt.
Bloss verschwendet das ziemlich viel Speicher. Also verwendet die C++ Implementierung 8 Bit grosse chars, und verwendet zwei 32 Bit Wörter für char Zeiger: ein Wort für die Adresse des 32 Bit Wortes in dem sich der "char" befindet, und noch ein Wort für die Nummer des "char" in diesem Wort (0-3).Ebenso sind dann Zeiger auf short 2 Worte gross, bzw. auch Zeiger auf alle Typen deren Alignment kleiner 4 Byte ist. Also ggf. auch Structs oder Klassen.
Angenommen OVERLAPPED wäre 2 Byte gross. Ein OVERLAPPED Zeiger würde dann aus 2 Worten bestehen.
Context enthält nun aber zusätzlich zu einem OVERLAPPED Member noch einen int. Nehmen wir an der int hat die üblichen 4 Byte, dann braucht ein "Context" min. 6 Byte. Sagen wir weiter die C++ Implementierung definiert dass alles was zwischen 5 und 7 Byte gross ist, mit Padding auf 8 Byte "aufgewüllt" wird. sizeof(Context) ist also 8, und alignof(Context) wäre sagen wir 4.
Und schwupps, ein "Zeiger auf Context" braucht nur mehr aus einem Wort zu bestehen (da alignof(Context) ja 4 ist, muss das 2. Wort immer 0 sein, und kann daher weggelassen werden).Resultat: ein "Zeiger auf OVERLAPPED" (T*) und ein "Zeiger auf Context" (U*) sehen im Speicher ganz unterschiedlich aus. Daher kann man auch nicht einen T** in einen U** casten, und erwarten, dass dabei etwas sinnvolles rauskommt. Das wäre wie einen std::vector<char>* in einen std::string* zu casten.
-
Nicht ablenkend gemeinter Applaus für diese informationsreiche Erklärung.

-
Hmmm...ganz so einfach ist es nicht. Eigentlich ist es mit allen Konsequenzen (strict aliasing!) sogar ziemlich kompliziert.
Also: Für POD-Structs (und auch nur für die) ist garantiert, dass die Speicheradresse des gesamten Structs und die seines ersten Members identisch sind - padding kommt nicht am Anfang eines structs. Dieser Umstand wird in C gern genutzt, um objektorientierte Vererbung per pointer punning nachzustellen - genau das, was du da vorhast. Im Standard:
ISO/IEC 14882:2003 9.2 (17) schrieb:
A pointer to a POD-struct object, suitably converted using a reinterpret_cast, points to its initial member (or if that member is a bit-field, then to the unit in which it resides) and vice versa. [Note: There might therefore be unnamed padding within a POD-struct object, but not at its beginning, as necessary to achieve appropriate alignment. ]
Natürlich musst du aber wie ein Adler darauf achten, dass du diesen Zeiger, wenn du ihn zurückcastest, nicht in einen Typ zurückcastest, der mit dem gespeicherten nicht Layout-kompatibel ist. Layout-Kompatibilität definiert wie folgt:
ISO/IEC 14482:2003 9.2 (14-16) schrieb:
14 Two POD-struct (clause 9) types are layout-compatible if they have the same number of nonstatic data members, and corresponding nonstatic data members (in order) have layout-compatible types (3.9).
15 Two POD-union (clause 9) types are layout-compatible if they have the same number of nonstatic data members, and corresponding nonstatic data members (in any order) have layout-compatible types (3.9).
16 If a POD-union contains two or more POD-structs that share a common initial sequence, and if the POD-union object currently contains one of these POD-structs, it is permitted to inspect the common initial part of any of them. Two POD-structs share a common initial sequence if corresponding members have layout-compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.
Und als ob das noch nicht fummelig genug wäre, gibt es strict-aliasing-Regeln nach 3.10 (15):
ISO/IEC 14882:2003 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 48):
— 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 (including, 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.48) The intent of this list is to specify those circumstances in which an object may or may not be aliased.
Das ist für den Optimierer wichtig. Beispielsweise darf der in einer Funktion der Form
void foo(bar *p, baz *q) { // ... }mit zwei POD-Typen bar und baz davon ausgehen, dass p und q nicht auf das gleiche Objekt zeigen. Das kann in Verbindung mit type punning, wenn man nicht sehr vorsichtig ist, zu sehr langen und frustrierenden Debugging-Sessions führen.
Insgesamt bedeutet das beispielsweise:
struct foo { OVERLAPPED o; int x; }; struct bar { OVERLAPPED o; short x; }; // ... foo f; OVERLAPPED *p = reinterpret_cast<OVERLAPPED*>(&f); bar *q = reinterpret_cast<bar*>(p); q->o; // Undefiniert! // Aber: union baz { foo f; bar b; } *r = reinterpret_cast<baz*>(p); r->b.o; // OK r->b.x; // Undefiniert! bar *s = &r->b; s->o; // Undefiniert! foo *t = &r->f; t->o; // OKDas ganze ist natürlich ein Wespennest, dass man am besten durch eine vernünftige Klassenhierarchie vollständig umgeht. Spricht etwas dagegen, einen Zeiger auf eine Basisklasse zu speichern und damit zu arbeiten? Dann kriegst du später auch keine Probleme damit, den Kram wieder zu löschen - momentan habe ich Schwierigkeiten zu sehen, wie du g_overlapped in definierter Weise wieder loswerden willst.
-
hustbaer schrieb:
Context* context; Get((OVERLAPPED**)&context);Warum macht man sowas?
Context* context; Get(&((OVERLAPPED*)context));Das wäre doch genau das, was man oben erreichen will, nur dass das Verhalten
definiert ist.Oder sehe ich das falsch?
-
Dankesehr für die Erklärungen!
So ganz verstehe ich es derzeit leider noch nicht. Besonders die Beispiele von @seldon ...
Da ich eh nur mit Windows arbeite, könnte ich es dann doch so stehen lassen.
@?????
error C2102: '&' requires l-value
-
Du kannst nur die Adresse von Objekten holen, die auch eine haben.
Beispiel:int* answer_to_the_universe = &42;Wird nicht funktionieren, du musst 42 in einer Variablen Speichern.
-
hustbaer schrieb:
Context* context; Get((OVERLAPPED**)&context);Das ist laut Standard nicht OK.
Dass es mit den meisten Implementierungen trotzdem funktionieren wird, ist eine andere Sache. Es gibt auch einige APIs die sich darauf verlassen dass soetwas funktioniert, z.B. die COM Funktionen unter Windows (CoCreateInstance, IUnknown::QueryInterface uvm.).So, ich muss mich hier mal selbst korrigieren.
CoCreateInstance etc. sind vollkommen OK, so lange man sie richtig verwendet.
OK:void* isv = 0; HRESULT hr = CoCreateInstance(...., &isv); ISomething* is = static_cast<ISomething*>(isv);Nicht OK:
ISomething* is = 0; HRESULT hr = CoCreateInstance(...., reinterpret_cast<void**>(&is));Die API an sich ist also in Ordnung, nur die Art wie sie meistens verwendet wird nicht.
-
Interessieren würde mich, ob es mit VS2008 definiert ist.
-
Hi schrieb:
Interessieren würde mich, ob es mit VS2008 definiert ist.
Wieso willst du es unbedingt so lassen? Schreib es um, so dass du kein undefiniertes Verhalten verwenden musst, anstatt dir den Kopf darüber zu zerbrechen ob eine bestimmte Implementierung das Verhalten definiert.
-
hustbaer schrieb:
void* isv = 0; HRESULT hr = CoCreateInstance(...., &isv); ISomething* is = static_cast<ISomething*>(isv);Wer Schreibarbeit sparen will, benutzt einen kleinen Proxy:
template<typename T> class APIfy { public: APIFy(T*& p) : p_( p ), v_( p ) {} ~APIFy() { p_ = static_cast< T* >( v_ ); } void** operator() { return &v_; } private: void operator=(APIfy&); T*& p_; void* v_; }; template <typename T> APIfy<T> apify(T*& p) { return APIfy<T>( p ); } ... ISomething* is = 0; HRESULT hr = CoCreateInstance(...., apify(is));Vorteil hier, dass man ggf. bereits vorhandenen Code durch einfaches Suchen&Ersetzen korrigieren kann.
-
@camper:
So einen Proxy verwenden wir in der Arbeit auch, nur dass er statt rohen Zeiger mit boost::intrusive_ptr<T> arbeitet