Arytmetyka wskaźnika na granicach podobiektów

Czy poniższy kod (który wykonuje arytmetykę wskaźników na granicach podobiektów) ma dobrze zdefiniowane zachowanie dla typówT dla których się kompiluje (co w C ++ 11,niekoniecznie musi to być POD) lub jego podzbiór?

#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 wykorzystuje odmianę powyższej techniki w implementacji wewnętrznego typu wektora, który jest zoptymalizowany do początkowego użycia stosu dla małych tablic, ale przełącza się na bufor przydzielony sterty po przekroczeniu początkowej pojemności. (Powód takiego postępowania nie wynika jasno z tego przykładu, ale najwyraźniej ma na celu zmniejszenie nadmiaru kodu szablonu; jest to wyraźniejsze, jeśli przejrzysz przezkod.)

UWAGA: Zanim ktokolwiek narzeka, nie jest to dokładnie to, co robią, i może być tak, że ich podejście jest bardziej zgodne ze standardami niż to, co podałem tutaj, ale chciałem zapytać o ogólny przypadek.

Oczywiście działa to w praktyce, ale jestem ciekawy, czy coś w standardzie gwarantuje, że tak będzie. Jestem skłonny powiedzieć „nie”, biorąc pod uwagęN3242 / expr.add:

Gdy odejmowane są dwa wskaźniki do elementów tego samego obiektu tablicowego, wynikiem jest różnica indeksów dolnych dwóch elementów tablicy ... Ponadto, jeśli wyrażenie P wskazuje na element obiektu tablicowego lub jeden na ostatni element obiektu tablicy, a wyrażenie Q wskazuje na ostatni element tego samego obiektu tablicy, wyrażenie ((Q) +1) - (P) ma taką samą wartość jak ((Q) - (P)) + 1 i jako - ((P) - ((Q) +1)) i ma wartość zero, jeśli wyrażenie P wskazuje jedną przeszłość ostatniego elementu obiektu tablicy, nawet jeśli wyrażenie (Q) +1 nie wskazuje na element obiektu tablicy. ... O ile oba wskaźniki nie wskazują elementów tego samego obiektu tablicowego lub jednego ostatniego elementu obiektu tablicy, zachowanie jest niezdefiniowane.

Ale teoretycznie środkowa część powyższego cytatu, w połączeniu z układem klas i gwarancjami wyrównania, może pozwolić, aby następująca (mniejsza) korekta była ważna:

#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;
}

co w połączeniu z różnymi innymi przepisami dotyczącymiunion układ, wymienialność na izchar *itd., może również sprawić, że oryginalny kod również będzie ważny. (Głównym problemem jest brak przechodniości w definicji arytmetyki wskaźników podanej powyżej).

Ktoś wie na pewno?N3242 / expr.add wydaje się jasne, że wskaźniki muszą należeć do tego samego „obiektu tablicy”, aby mógł być zdefiniowany, ale tomógłby hipotetycznie jest tak, że inne gwarancje w standardzie, gdy są połączone razem, mogą wymagać i tak definicji w tym przypadku, aby zachować logiczną spójność. (Nie stawiam na to, ale przynajmniej byłoby to możliwe.)

EDYTOWAĆ: @MatthieuM podnosi zastrzeżenie, że ta klasa nie jest układem standardowym i dlatego może nie mieć gwarancji, że nie będzie zawierać dopełnienia między podobiektem podstawowym a pierwszym elementem pochodnym, nawet jeśli oba są wyrównane doalignof(T). Nie jestem pewien, czy to prawda, ale otwiera następujące pytania wariantowe:

Czy byłoby to zagwarantowane, gdyby spadek został usunięty?

By&d.end - &d.begin >= sizeof(float) * 10 być gwarantowane, nawet jeśli&d.end - &d.begin == sizeof(float) * 10 nie były?

LAST EDIT @ ArneMertz opowiada się za bardzo bliską lekturąN3242 / expr.add (tak, wiem, że czytam wersję roboczą, ale jest ona wystarczająco bliska), ale czy standard naprawdę sugeruje, że następujące zachowanie ma niezdefiniowane zachowanie, jeśli linia wymiany zostanie usunięta? (te same definicje klas, co powyżej)

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;
}

Także jeśli== nie jest wystarczająco silny, jeśli skorzystamy z tegostd::less tworzy całkowity porządek nad wskaźnikami i zmienia warunkowe powyżej na:

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

Czy kod, który zakłada, że ​​dwa równe wskaźniki wskazują na ten sam obiekt tablicowy, naprawdę uszkodzony zgodnie ze ścisłym odczytem standardu?

EDYTOWAĆ Przepraszam, chcę dodać jeszcze jeden przykład, aby wyeliminować standardowy problem z układem:

#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;
}

questionAnswers(1)

yourAnswerToTheQuestion