Подтвердить что ты не робот

Арифметика указателя по границам подобъектов

Имеет ли следующий код (который выполняет арифметику указателей на границах подобъектов) корректное поведение для типов T, для которых он компилируется (что в С++ 11, необязательно должен быть POD) или его подмножеством?

#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 использует вариацию вышеупомянутого метода при реализации внутреннего векторного типа, который оптимизирован для первоначального использования стека для небольших массивов, но переключается на буфер, распределенный по кучам, после первоначальной емкости. (Причина этого не очевидна из этого примера, но, по-видимому, для уменьшения размытости кода шаблона, это яснее, если вы просмотрите code.)

ПРИМЕЧАНИЕ.. Прежде чем кто-либо пожалуется, это не совсем то, что они делают, и может быть, что их подход более соответствует стандартам, чем то, что я здесь дал, но я хотел спросить о общий случай.

Очевидно, что это работает на практике, но мне любопытно, если что-либо в стандартных гарантиях для этого. Я склонен сказать "нет", учитывая N3242/expr.add:

Когда два указателя на элементы одного и того же объекта массива вычитаются, результатом является разность индексов двух элементов массива... Более того, если выражение P указывает либо на элемент объекта массива, либо на одно прошлое последний элемент объект массива, а выражение Q указывает на последний элемент одного и того же объекта массива, выражение ((Q) +1) - (P) имеет то же значение, что и ((Q) - (P)) + 1, и как - ((P) - ((Q) +1)) и имеет значение 0, если выражение P указывает один за последним элементом объекта массива, хотя выражение (Q) +1 не указывает на элемент объекта массива.... Если оба указателя не указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, поведение undefined.

Но теоретически средняя часть приведенной выше цитаты, в сочетании с макетом класса и гарантиями выравнивания, может разрешить следующую (младшую) настройку:

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

который в сочетании с различными другими положениями, касающимися макета union, конвертируемости в char * и т.д., может, возможно, также сделать исходный код действительным. (Основная проблема заключается в отсутствии транзитивности в определении арифметики указателя, приведенной выше.)

Кто-нибудь знает наверняка? N3242/expr.add, по-видимому, ясно указывает, что указатели должны принадлежать к одному и тому же "объекту массива" для его определения, но гипотетически может быть, что другие гарантии в стандарте, когда они объединены вместе, могут потребовать определения в любом случае в этот случай, чтобы оставаться логически самосогласованным. (Я не ставлю на него, но я бы это по крайней мере мыслил.)

EDIT: @MatthieuM выдвигает возражение о том, что этот класс не является стандартным макетом и, следовательно, не может быть гарантированно не содержать отступов между базовым подобъектом и первым членом производного, даже если оба выровнены с alignof(T). Я не уверен, насколько это верно, но это открывает следующие варианты вопросов:

  • Будет ли гарантировано работать, если удаление было удалено?

  • Было бы гарантировано &d.end - &d.begin >= sizeof(float) * 10, даже если &d.end - &d.begin == sizeof(float) * 10 не были?

LAST EDIT @ArneMertz утверждает, что очень внимательно читает N3242/expr.add(да, я знаю, что я читаю черновик, но он достаточно близко), но действительно ли стандарт подразумевает что следующее имеет поведение undefined, если линия подкачки удалена? (такие же определения классов, как указано выше)

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

Кроме того, если == недостаточно силен, что, если мы воспользуемся тем фактом, что std::less образует полный порядок над указателями и меняет условное выражение на:

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

Является ли код, предполагающий, что два равных указателя указывают на тот же самый объект массива, который действительно нарушен в соответствии со строгим чтением стандарта?

РЕДАКТИРОВАТЬ Извините, просто добавьте еще один пример, чтобы устранить проблему стандартного макета:

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

Ответ 1

Обновлено: Этот ответ сначала пропустил некоторую информацию и, следовательно, привел к неправильным выводам.

В ваших примерах initial и rest представляют собой явно различные (массивные) объекты, поэтому сравнение указателей на initial (или его элементов) с указателями на rest (или его элементы) равно

  • UB, если вы используете разницу указателей. (§5.7,6)
  • неуказан, если вы используете реляционные операторы (§5.9,2)
  • четко определен для == (Итак, второй отрезанный хорошо, см. ниже)

Первый фрагмент:

Построение разницы в первом фрагменте - это поведение undefined, для указанной вами цитаты (§5.7,6):

Если оба указателя не указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, поведение undefined.

Чтобы прояснить части UB первого примерного кода:

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

Интересна строка, отмеченная (*): d.begin и d.end не являются элементами одного и того же массива, и поэтому операция приводит к UB. Это несмотря на то, что вы можете reinterpret_cast<char*>(&d) и иметь оба адреса в результирующем массиве. Но поскольку этот массив является представлением всех d, его нельзя рассматривать как доступ к частям d. Поэтому, хотя эта операция, вероятно, будет работать и даст ожидаемый результат на любой реализации, о которой вы можете мечтать, она по-прежнему является UB - как определение.

Второй фрагмент:

Это действительно хорошо определенное поведение, но результат реализации определен:

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

Строка, помеченная (!), не является ub, но ее результат определяется реализацией, так как выполнение, выравнивание и упомянутая установка могут сыграть свою роль. Но если это утверждение будет выполнено, , вы можете использовать две части объекта, например, один массив.

Вы знали бы, что rest[0] будет размещаться сразу после initial[0] в памяти. На первый взгляд вы не могли легко использовать равенство:

  • initial[1] будет указывать один на один конец initial, разыменовывая это UB.
  • rest[-1] явно выходит за пределы.

Но вводится §3.9.2,3:

Если объект типа T расположен по адресу A, указатель типа cv T*, значением которого является адрес A, как говорят, указывает на этот объект, независимо от того, как это значение было получено. [Примечание: Например, адрес, следующий за концом массива (5.7), будет считаться указывать на несвязанный объект тип массивов, который может быть расположен по этому адресу.

Таким образом, при условии, что &initial[1] == &rest[0], он будет двоичным, как если бы был только один массив, и все будет нормально.

Вы можете выполнять итерацию по обоим массивам, поскольку на границах можно применить некоторый "переключатель контекста указателя". Итак, к вашему последнему фрагменту: swap не нужен!

Однако есть некоторые оговорки: rest[-1] - UB, и поэтому было бы initial[2], из-за §5.7,5:

Если оба операнда указателя и результат указывают на элементы одного и того же объекта массива или одно прошлое последний элемент объекта массива, оценка не должна приводить к переполнению; в противном случае, поведение undefined.

(акцент мой). Итак, как эти два сочетаются?

  • "Хороший путь": &initial[1] - это нормально, и поскольку &initial[1] == &rest[0] вы можете принять этот адрес и продолжить, чтобы увеличить указатель, чтобы получить доступ к другим элементам rest, из-за §3.9.2,3
  • "Плохой путь": initial[2] is *(initial + 2), но так как §5.7,5, initial +2 уже UB, и вы никогда не сможете использовать §3.9.2.3 здесь.

Вместе: вам нужно остановиться на границе, сделать короткий перерыв, чтобы проверить, что адреса равны, и затем вы можете двигаться дальше.