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

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

РЕДАКТИРОВАТЬ: @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))))

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

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

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

24

Решение

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

В твоих примерах 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указатель типа резюме 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 здесь.

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

8

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]