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

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

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

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

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

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

#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 не будет?

ПОСЛЕДНЕЕ РЕДАКТИРОВАНИЕ @ArneMertz выступает за очень внимательное прочтение N3242/expr.add (да, я знаю, что читаю черновик, но это достаточно близко), но стандарт действительно подразумевает, что следующее имеет неопределенное поведение, если удалить строку подкачки? (те же определения классов, что и выше)

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))))

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

EDIT Извините, просто хочу добавить еще один пример, чтобы устранить проблему со стандартным макетом:

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

person Community    schedule 05.03.2013    source источник
comment
Я не могу поверить, что в теге C++ есть хорошие вопросы. +1.   -  person    schedule 05.03.2013
comment
Возможно, это дубликат выравнивания элемента Union, но я не уверен   -  person BЈовић    schedule 05.03.2013
comment
@BЈовић этот вопрос предполагает понимание ответа на этот вопрос, собственно   -  person Stephen Lin    schedule 05.03.2013
comment
@StephenLin: я должен признать, что нахожу этот код сомнительным; Я сомневаюсь, что в стандарте есть какая-либо гарантия того, что заполнение не может быть вставлено между базовым объектом и первым атрибутом производного объекта (хотя это было бы глупо...)   -  person Matthieu M.    schedule 05.03.2013
comment
@MatthieuM хорошо, это не мой код, и я думаю, что этого может быть гарантировано, по крайней мере, для POD, а возможно, и в других случаях, но я не уверен.   -  person Stephen Lin    schedule 05.03.2013
comment
@StephenLin: определение класса стандартного макета (который представляет собой расширенную структуру C++11) дано в §9/7; в частности, требуется либо не иметь нестатических элементов данных в наиболее производном классе и не более одного базового класса с нестатическими элементами данных, либо не иметь базовых классов с нестатическими элементами данных, что, очевидно, здесь не так.   -  person Matthieu M.    schedule 05.03.2013
comment
@MatthieuM хорошо, вы могли бы сделать это без наследования, и это все равно был бы правильный вопрос ... можете ли вы выполнить арифметику между двумя соседними массивами?   -  person Stephen Lin    schedule 05.03.2013
comment
@MatthieuM, кроме того, даже если нет гарантии равенства, есть ли гарантия, что разница будет хотя бы такой большой?   -  person Stephen Lin    schedule 05.03.2013
comment
@StephenLin: я сомневаюсь, что вы можете сделать это и с другими массивами. Еще раз из-за возможных проблем с заполнением. Например, представьте себе инструментарий сборки, оставляя красные зоны вокруг каждого массива для обнаружения доступа за его пределами. И в Стандарте довольно ясно, что теоретически два разных объекта подразумевают неопределенное поведение (остаток ближних/дальних указателей и их разные адресные пространства, я полагаю).   -  person Matthieu M.    schedule 05.03.2013
comment
@MatthieuM. хорошо, ну, я не говорил, что думал, что это гарантировано, мне просто любопытно, знает ли кто-нибудь наверняка. Может ты и прав, но я не знаю.   -  person Stephen Lin    schedule 05.03.2013
comment
@StephenLin: никогда не был уверен в запутанности Стандарта, поэтому я использовал комментарии вместо простого ответа :)   -  person Matthieu M.    schedule 05.03.2013
comment
@MatthieuM. в любом случае, может случиться так, что положения стандарта, взятые в целом, могут логически не допустить, чтобы неравенство &d.end - &d.begin >= sizeof(float) * 10, по крайней мере, не выполнялось, даже если операция строго не определена изолированно . возможно?   -  person Stephen Lin    schedule 05.03.2013
comment
Массивы нулевой длины недействительны.   -  person R. Martinho Fernandes    schedule 05.03.2013
comment
@R.MartinhoFernandes Я полагаю, что это опечатка и должно быть T initial[1];, так как он хочет один элемент в initial, а остальные в rest. В противном случае использование &d.initial[1] не имело бы смысла (или было бы UB, если бы ZLA были разрешены)   -  person Arne Mertz    schedule 05.03.2013
comment
@ArneMertz о, да, это была опечатка   -  person Stephen Lin    schedule 05.03.2013


Ответы (1)


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

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

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

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

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

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

Чтобы прояснить части 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:

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

(выделено мной). Так как же эти двое сочетаются?

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

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

person Arne Mertz    schedule 05.03.2013
comment
Я верю вам, но это означало бы, что невозможно реализовать std::memset или что-то подобное самостоятельно, не вызывая тогда UB, независимо от того, был ли объект стандартной компоновки или нет? Вам придется использовать предоставленные функции в качестве примитивов или что-то еще? - person Stephen Lin; 05.03.2013
comment
(Кстати, мое редактирование было отклонено, но я думаю, что вы имеете в виду float *, а не int *) - person Stephen Lin; 05.03.2013
comment
Я приму это, если вы уточните, считаете ли вы, что последний пример в моем отредактированном вопросе - это UB или нет. (Хотя технически это может быть просто любопытно, что вы думаете.) - person Stephen Lin; 06.03.2013
comment
@StephenLin: memset полагается на возможность reinterpret_cast<char> что угодно и свойства POD (или стандартных типов макетов, не уверен, что это так). Объекты Memsetting, у которых нет этих свойств, действительно являются UB. - Я исправил тип poitner - понятия не имею, почему ваше редактирование было отклонено. Я прокомментирую последнее редактирование в своем ответе. - person Arne Mertz; 06.03.2013
comment
отлично, интересно узнать, что вы думаете. Кроме того, по-видимому, сейчас это не стандартный макет из-за наследования, но если бы это было удалено, тогда, я думаю, это было бы. Так не будет ли тогда, что &begin и &end будут двумя указателями на массив с четко определенной компоновкой? иначе как было бы безопасно выполнять побитовое копирование между стандартными объектами макета через указатели char *? - person Stephen Lin; 06.03.2013
comment
Я только что нашел часть стандарта, которая противоречит моему ответу. Написание займет минуту. Быть в курсе... - person Arne Mertz; 06.03.2013
comment
хорошо, спасибо, что нашли время, чтобы прочитать вещи; Я соглашусь завтра, когда у меня будет шанс (думаю, я дам награду, чтобы получить больше мнений, хотя не откажусь от вашего ответа, чтобы сделать это) - person Stephen Lin; 06.03.2013
comment
Нет проблем, я получаю удовольствие, разбираясь в таких вещах из стандарта. - person Arne Mertz; 06.03.2013