Имеет ли следующий код (который выполняет арифметику указателей через границы подобъектов) хорошо определенное поведение для типов 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;
}
Обновлено: Этот ответ сначала пропустил некоторую информацию и, следовательно, привел к неверным выводам.
В твоих примерах initial
а также rest
являются четко различимыми (массивными) объектами, поэтому сравнение указателей с initial
(или его элементы) с указателями на rest
(или его элементы)
==
(Так что второй отрезанный — это хорошо, см. Ниже)Создание различий в первом фрагменте — неопределенное поведение для предоставленной вами цитаты (§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,3initial[2]
является *(initial + 2)
, но с §5.7,5, initial +2
уже UB, и вы никогда не сможете использовать §3.9.2,3 здесь. Вместе: вы должны зайти на границу, сделать небольшой перерыв, чтобы убедиться, что адреса совпадают, и затем вы можете двигаться дальше.
Других решений пока нет …