"Pointerfrickeleien" - Ist das undefiniert?



  • 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; // OK
    

    Das 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.


  • Mod

    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 🙂


Anmelden zum Antworten