Aritmética de ponteiro através dos limites do subobjeto

O código a seguir (que executa a aritmética de ponteiro entre os limites do subobjeto) tem um comportamento bem definido para tiposT para o qual compila (que, em C ++ 11,não necessariamente tem que ser POD) ou qualquer subconjunto dele?

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

O LLVM usa uma variação da técnica acima na implementação de um tipo de vetor interno que é otimizado para inicialmente usar a pilha para matrizes pequenas, mas alterna para um buffer alocado para heap uma vez acima da capacidade inicial. (A razão para fazer isso dessa maneira não está clara neste exemplo, mas é aparentemente para reduzir o inchaço do código do modelo; isso fica mais claro se você olharcódigo.)

NOTA: Antes que alguém reclame, isso não é exatamente o que eles estão fazendo e pode ser que a abordagem deles seja mais compatível com os padrões do que o que eu dei aqui, mas eu queria perguntar sobre o caso geral.

Obviamente, funciona na prática, mas estou curioso para saber se alguma coisa no padrão garante que seja esse o caso. Estou inclinado a dizer não, dadoN3242 / expr.add:

Quando dois ponteiros para elementos do mesmo objeto de matriz são subtraídos, o resultado é a diferença dos subscritos dos dois elementos de matriz ... Além disso, se a expressão P aponta para um elemento de um objeto de matriz ou um passado do último elemento de um objeto de matriz, e a expressão Q aponta para o último elemento do mesmo objeto de matriz, a expressão ((Q) +1) - (P) tem o mesmo valor que ((Q) - (P)) + 1 e como - ((P) - ((Q) +1)), e tem o valor zero se a expressão P apontar um passado para o último elemento do objeto da matriz, mesmo que a expressão (Q) +1 não aponte para um elemento do objeto da matriz. ... A menos que ambos os ponteiros apontem para elementos do mesmo objeto de matriz ou um após o último elemento do objeto de matriz, o comportamento é indefinido.

Mas, teoricamente, a parte central da cotação acima, combinada com o layout da classe e as garantias de alinhamento, pode permitir que o seguinte ajuste (menor) seja válido:

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

que combinado com várias outras disposições relativasunion layout, conversibilidade de e parachar *, etc., pode indiscutivelmente tornar o código original válido também. (O principal problema é a falta de transitividade na definição da aritmética de ponteiros dada acima).

Alguém sabe com certeza?N3242 / expr.add parece deixar claro que os ponteiros devem pertencer ao mesmo "objeto array" para que seja definido, maspoderia hipoteticamente, o fato de que outras garantias no padrão, quando combinadas, podem exigir uma definição, de qualquer maneira, neste caso, a fim de permanecer logicamente auto-consistente. (Eu não estou apostando nisso, mas eu seria pelo menos concebível.)

EDITAR: @MatthieuM levanta a objeção de que essa classe não é de layout padrão e, portanto, pode não ser garantido que não contenha nenhum preenchimento entre o subobjeto base e o primeiro membro do derivado, mesmo que ambos estejam alinhados paraalignof(T). Não tenho certeza de como isso é verdade, mas isso abre as seguintes perguntas variantes:

Isso seria garantido para funcionar se a herança fosse removida?

Seria&d.end - &d.begin >= sizeof(float) * 10 ser garantido mesmo se&d.end - &d.begin == sizeof(float) * 10 não estivessem?

ÚLTIMA EDIT @ArneMertz defende uma leitura muito próximaN3242 / expr.add (sim, eu sei que estou lendo um rascunho, mas está perto o suficiente), mas o padrão realmente implica que o seguinte tenha um comportamento indefinido se a linha de swap for removida? (mesmas definições de classe acima)

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

Também se== não é forte o suficiente, e se aproveitarmos o fato de questd::less forma um pedido total sobre ponteiros e altera o condicional acima para:

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

É o código que assume que dois ponteiros iguais apontam para o mesmo objeto de matriz realmente quebrado de acordo com uma leitura estrita do padrão?

EDITAR Desculpe, só quero adicionar mais um exemplo para eliminar o problema de layout padrão:

#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