Zeigerarithmetik über Unterobjektgrenzen hinweg

Hat der folgende Code (der Zeigerarithmetik über Unterobjektgrenzen hinweg ausführt) ein genau definiertes Verhalten für TypenT für die es kompiliert (die in C ++ 11,muss nicht unbedingt POD sein) oder eine Teilmenge davon?

#include <cassert>
#include <cstddef>

template<typename T>
struct Base
{
    // ensure alignment
    union
    {
        T initial;
        char begin;
    };
};

template<typename T, size_t N>
struct Derived : public Base<T>
{
    T rest[N - 1];
    char end;
};

int main()
{
    Derived<float, 10> d;
    assert(&d.rest[9] - &d.initial == 10);
    assert(&d.end - &d.begin == sizeof(float) * 10);
    return 0;
}

LLVM verwendet eine Variation der obigen Technik bei der Implementierung eines internen Vektortyps, der optimiert ist, um den Stapel anfangs für kleine Arrays zu verwenden, aber einmal über die anfängliche Kapazität auf einen Heap-zugewiesenen Puffer umschaltet. (Der Grund für diese Vorgehensweise ist in diesem Beispiel nicht klar, liegt aber anscheinend in der Reduzierung des aufgeblähten Vorlagencodes. Dies ist klarer, wenn Sie durch dieCode.)

HINWEIS: Bevor sich jemand beschwert, ist dies nicht genau das, was er tut, und es könnte sein, dass sein Ansatz normgerechter ist als das, was ich hier angegeben habe, aber ich wollte nach dem allgemeinen Fall fragen.

Natürlich funktioniert es in der Praxis, aber ich bin gespannt, ob irgendetwas im Standard dafür spricht. Ich bin geneigt, nein zu sagen, gegebenN3242 / expr.add:

Wenn zwei Zeiger auf Elemente desselben Array-Objekts subtrahiert werden, ist das Ergebnis die Differenz der Indizes der beiden Array-Elemente. Außerdem zeigt der Ausdruck P entweder auf ein Element eines Array-Objekts oder auf eines nach dem letzten Element eines Array-Objekts, und der Ausdruck Q zeigt auf das letzte Element desselben Array-Objekts, hat der Ausdruck ((Q) +1) - (P) den gleichen Wert wie ((Q) - (P)) + 1 und as - ((P) - ((Q) +1)) und hat den Wert Null, wenn der Ausdruck P eins nach dem letzten Element des Array-Objekts zeigt, obwohl der Ausdruck (Q) +1 nicht auf ein zeigt Element des Array-Objekts. ... Wenn beide Zeiger nicht auf Elemente desselben Array-Objekts oder nach dem letzten Element des Array-Objekts zeigen, ist das Verhalten undefiniert.

Theoretisch kann der mittlere Teil des obigen Zitats in Kombination mit den Garantien für das Layout und die Ausrichtung der Klassen die folgende (geringfügige) Anpassung zulassen:

#include <cassert>
#include <cstddef>

template<typename T>
struct Base
{
    T initial[1];
};

template<typename T, size_t N>
struct Derived : public Base<T>
{
    T rest[N - 1];
};

int main()
{
    Derived<float, 10> d;
    assert(&d.rest[9] - &d.rest[0] == 9);
    assert(&d.rest[0] == &d.initial[1]);
    assert(&d.rest[0] - &d.initial[0] == 1);
    return 0;
}

die mit verschiedenen anderen Bestimmungen in Bezug kombiniertunion Layout, Konvertierbarkeit von und nachchar *usw. machen möglicherweise auch den Originalcode gültig. (Das Hauptproblem ist die mangelnde Transitivität bei der oben angegebenen Definition der Zeigerarithmetik.)

Weiß jemand sicher Bescheid?N3242 / expr.add scheint klar zu machen, dass Zeiger zum selben "Array-Objekt" gehören müssen, damit es definiert wird, aber eskönnte Hypothetisch ist der Fall, dass andere Garantien in der Norm, wenn sie miteinander kombiniert werden, in diesem Fall ohnehin eine Definition erfordern könnten, um logisch in sich schlüssig zu bleiben. (Ich wette nicht darauf, aber ich würde es zumindest vorstellen.)

BEARBEITEN: @MatthieuM erhebt den Einwand, dass diese Klasse kein Standardlayout ist und daher möglicherweise keine Auffüllung zwischen dem Basis-Unterobjekt und dem ersten Element des abgeleiteten Objekts enthält, selbst wenn beide an ausgerichtet sindalignof(T). Ich bin mir nicht sicher, wie wahr das ist, aber das wirft die folgenden Variantenfragen auf:

Würde dies garantiert funktionieren, wenn die Vererbung entfernt würde?

Würde&d.end - &d.begin >= sizeof(float) * 10 garantiert werden, auch wenn&d.end - &d.begin == sizeof(float) * 10 waren nicht?

LAST EDIT @ArneMertz plädiert für eine sehr enge Lektüre vonN3242 / expr.add (Ja, ich weiß, ich lese einen Entwurf, aber er ist nah genug), aber impliziert der Standard wirklich, dass das folgende Verhalten undefiniert ist, wenn die Auslagerungslinie entfernt wird? (gleiche Klassendefinitionen wie oben)

int main()
{
    Derived<float, 10> d;
    bool aligned;
    float * p = &d.initial[0], * q = &d.rest[0];

    ++p;
    if((aligned = (p == q)))
    {
        std::swap(p, q); // does it matter if this line is removed?
        *++p = 1.0;
    }

    assert(!aligned || d.rest[1] == 1.0);

    return 0;
}

Auch wenn== ist nicht stark genug, was ist, wenn wir die Tatsache ausnutzen, dassstd::less bildet eine Gesamtreihenfolge über Zeigern und ändert die obige Bedingung in:

    if((aligned = (!std::less<float *>()(p, q) && !std::less<float *>()(q, p))))

Ist Code, der davon ausgeht, dass zwei gleiche Zeiger auf dasselbe Array-Objekt verweisen, nach striktem Lesen des Standards wirklich fehlerhaft?

BEARBEITEN Entschuldigung, ich möchte nur ein weiteres Beispiel hinzufügen, um das Problem mit dem Standardlayout zu beheben:

#include <cassert>
#include <cstddef>
#include <utility>
#include <functional>

// standard layout
struct Base
{
    float initial[1];
    float rest[9];
};

int main()
{
    Base b;
    bool aligned;
    float * p = &b.initial[0], * q = &b.rest[0];

    ++p;
    if((aligned = (p == q)))
    {
        std::swap(p, q); // does it matter if this line is removed?
        *++p = 1.0;
        q = &b.rest[1];
        // std::swap(p, q); // does it matter if this line is added?
        p -= 2; // is this UB?
    }
    assert(!aligned || b.rest[1] == 1.0);
    assert(p == &b.initial[0]);

    return 0;
}

Antworten auf die Frage(1)

Ihre Antwort auf die Frage