Layout der Daten-Member bei Vererbung für Klassen mit Standard Layout



  • Hallo zusammen!

    Inspiriert von diesem Vortrag schaue ich mir derzeit an, wie das LLVM-Projekt Ihren SmallVector konkret implementiert hat (für Code siehe hier).
    Dabei ist mir eine Technik ins Auge gesprungen, von der ich mich gerade zu überzeugen versuche, dass sie korrekt im Sinne des C++-Standards ist:

    Dabei wird ein zusammenhängendes Array auf zwei Klassen verteilt, wobei die eine Klasse von der anderen erbt. Das erste Element des Arrays befindet
    sich dabei in der Basisklasse, die restlichen Elemente in der abgeleiteten Klasse. Ich habe diese Technik zur besseren Anschlaulichkeit mal vesucht auf
    ihre Essenz zu destillieren (so wie ich den Code verstanden habe):

    template <typename T>
    struct A
    {
        static_assert(std::is_standard_layout<T>::value, "T must have standard layout.");
    
        std::size_t size;
        T first_element;
        // Aus LLVMs SmallVector.h:
        // Space after 'FirstEl' is clobbered, do not add any instance vars after it.
    
        T& operator[](std::size_t i)
        {
            assert(i < size);
            return *(&first_element + i);
        }
    
    protected:
        A(std::size_t size) : size{ size } { }
    };
    
    template <typename T, std::size_t N>
    struct B : A<T>
    {
        static_assert(N > 1, "N must be > 1.");
    
        T other_elements[N - 1];
    
        B() : A{ N } { }
    };
    

    Nun zu meiner Fage: Ist dieser Code korrekt und wenn ja warum?

    Meine Vermutung ist, dass dieser Code korrekt ist, wenn A, B und T Standard Layout haben, und meine Recherchen sind bisher in diese Richtung gegangen,
    allerdings tue ich mich noch etwas schwer aus den ganzen Definitionen herauszudestillieren, dass first_element und other_elements wirklich zusammenhängend
    sein müssen, so als wenn sie als ein einzelnes Array deklariert worden wären. Was ist z.B. wenn nach first_element Padding erforderlich wäre? So wird beispielsweise
    hier bei einem ähnlichen Problem argumentiert, dass es keine Garantie gäbe, allerdings gehe ich auch davon aus, dass sie LLVM-Leute halbwegs wissen, was sie
    tun und dass sich eine solche Garantie irgendwie herleiten lassen muss.

    Vielleicht hat ja einer von euch ein paar gute Argumente, die meine Zweifel ausräumen - ansonsten suche ich selbst gerade auch noch weiter nach einer guten Begründung 😃

    Gruss,
    Finnegan



  • Da es um eine LLVM Implementierung geht, muss es ja nicht unbedingt allgemein garantiert sein.
    Ich denke, dass der Standard da durchaus Padding erlaubt.



  • manni66 schrieb:

    Da es um eine LLVM Implementierung geht, muss es ja nicht unbedingt allgemein garantiert sein.
    Ich denke, dass der Standard da durchaus Padding erlaubt.

    Nun, die nutzen diese Klasse sehr intensiv und LLVM soll man auch mit anderen Compilern als Clang bauen können.

    Finnegan

    P.S.: Könnte natürlich auch so ein Ding sein, wo so ziemlich alle Compiler das "richtige" tun, auch wenn es der Standard nicht garantiert.



  • Hi nochmal!

    Die Sache mit dem Standard Layout ist Bockmist, da ich eine wichtige Regel überlesen habe: B ist nicht Standard Layout,
    da für seinen Ableitungspfad in mehr als einer Klasse nicht-statische Daten-Member deklariert werden.

    Ich bin mittlerweile auch zu dem Schluss gekommen, dass der Standard wohl keine zusammenhängenden T s garantiert,
    und glaube dass die Argumentation, weshalb dieser Code allgemein funktioniert, wesentlich trivialier und "hackiger" ist,
    als ich zunächst angenommen habe:

    first_element und other_elements sind in der eigentlichen Implementierung nicht vom Typ T , sondern de factor char-Arrays
    mit korrektem Alignment, in deren Speicher die eigentlichen Objekte bei Bedarf konstruiert werden (ähnlich std::aligned_storage ).
    Das bedeutet, dass man selbst wenn Padding stattfindet, ab Adresse &first_element genügend Speicher vorhanden ist, um das
    Array aufzunehmen. Das gilt allerdings nur, wenn im Layout von B das Objekt other_elements direkt auf first_element folgt,
    und das ist gemäß Standard explizit nicht spezifiziert:

    10.0.5
    The order in which the base class subobjects are allocated in the most derived object (1.8) is unspecified. [...]

    Anmerkung: mit "base class subobjects" sind die gesamten Basisklassen als Objekte gemeint, und nicht etwa nur die Member der Basisklassen.

    Tatsächlich gibt der Standard nur sehr wenige Zusicherungen her, was ds Speicherlayout allgemeiner Klassen angeht.
    Interessant ist allerdings dieser Punkt:

    9.2.13
    Nonstatic data members of a (non-union) class with the same access control (Clause 11) are allocated so
    that later members have higher addresses within a class object. The order of allocation of non-static data
    members with different access control is unspecified (Clause 11). Implementation alignment requirements
    might cause two adjacent members not to be allocated immediately after each other; so might requirements
    for space for managing virtual functions (10.3) and virtual base classes (10.1).

    Daraus ziehe ich folgende Schlüsse:

    • first_element hat innerhalb von A (und somit auch im A -Teil von B ) die höchste Adresse, ist also das letzte Objekt in A (da alle Daten-Member mit dem selben Zugriffsmodifikator)
      - zwischen den Daten-Membern können höchstens Padding-Bytes liegen (da keine virtuelle Vererbung)

    So wie ich das sehe, bleibt also nur noch die nicht-garantierte Annahme, dass für B immer &first_element < &other_elements[0] gilt, wovon ich
    vermute, dass das bei gängigen Compilern der Fall sein wird (lässt sich ggfs. via static_assert prüfen). Das ist nicht so sauber wie ich erhofft hatte,
    aber zumindest etwas mit dem ich experimentieren würde, sollten es die Vorteile es rechtfertigen.

    Finnegan


Log in to reply