Имеет ли следующий код (который выполняет арифметические операции с указателями через границы подобъектов) четко определенное поведение для типов 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;
}
&d.end - &d.begin >= sizeof(float) * 10
, по крайней мере, не выполнялось, даже если операция строго не определена изолированно . возможно? - person Stephen Lin   schedule 05.03.2013T initial[1];
, так как он хочет один элемент вinitial
, а остальные вrest
. В противном случае использование&d.initial[1]
не имело бы смысла (или было бы UB, если бы ZLA были разрешены) - person Arne Mertz   schedule 05.03.2013