В вашем примере это *(p1 + 1) = 10;
должно быть UB, потому что это один за концом массива размера 1. Но мы находимся в очень особом случае здесь, потому что массив был динамически построен в большем массиве char.
Создание динамических объектов описано в 4.5 Объектная модель C ++ [intro.object] , §3 черновика n4659 стандарта C ++:
3 Если в хранилище создается полный объект (8.3.4), связанный с другим объектом e типа «массив из N символов без знака» или типа «массив из N std :: byte» (21.2.1), этот массив обеспечивает хранилище для созданного объекта, если:
(3.1) - время жизни e началось и не закончилось, и
(3.2) - хранилище для нового объекта полностью помещается в e, и
(3.3) - нет меньшего объекта массива, который удовлетворяет этим ограничения.
Версия 3.3 кажется довольно неясной, но примеры ниже проясняют замысел:
struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B;
int *p = new (b->b + 4) int;
Итак, в этом примере buffer
массив предоставляет хранилище для *p1
и *p2
.
Следующие пункты доказывают , что законченным объектом для обоих *p1
и *p2
является buffer
:
4 Объект a вложен в другой объект b, если:
(4.1) - a является подобъектом b, или
(4.2) - b обеспечивает хранилище для a, или
(4.3) - существует объект c, где a вложено в c , а c вложен в b.
5 Для каждого объекта x существует некоторый объект, называемый полным объектом x, который определяется следующим образом:
(5.1) - Если x является полным объектом, то полный объект x является самим собой.
(5.2) - В противном случае полный объект x является полным объектом (уникального) объекта, который содержит x.
Как только это будет установлено, другой соответствующей частью проекта n4659 для C ++ 17 будет [basic.coumpound] §3 (выделите мой):
3 ... Каждое значение типа указателя является одним из следующих:
(3.1) - указатель на объект или функцию (говорят, что указатель указывает на объект или функцию), или
(3.2) - указатель за концом объекта (8.7), или
(3.3) - значение нулевого указателя (7.11) для этого типа, или
(3.4) - недопустимое значение указателя.
Значение типа указателя, которое является указателем на конец объекта или за ним, представляет адрес первого байта в памяти (4.4), занятого объектом, или первого байта в памяти после конца памяти,
занятой объектом. соответственно. [Примечание: указатель за концом объекта (8.7) не считается указывающим на несвязанныйобъект типа объекта, который может находиться по этому адресу. Значение указателя становится недействительным, когда память, которую он обозначает, достигает конца срока хранения; см. 6.7. - конец примечания] В целях арифметики указателей (8.7) и сравнения (8.9, 8.10) указатель за концом последнего элемента массива x из n элементов считается эквивалентным указателю на гипотетический элемент x [ п]. Представление значений типов указателей определяется реализацией. Указатели на типы, совместимые с макетом, должны иметь одинаковые требования к представлению значений и выравниванию (6.11) ...
Примечание Указатель за концом ... здесь не применяется, потому что объекты, на которые указывает p1
и p2
не являются несвязанными , но вложены в один и тот же полный объект, поэтому арифметика указателей имеет смысл внутри объекта, который обеспечивает хранилище: p2 - p1
определен и является (&buffer[sizeof(int)] - buffer]) / sizeof(int)
то есть 1.
Так p1 + 1
есть указатель *p2
, и *(p1 + 1) = 10;
имеет определенное поведение и устанавливает значение *p2
.
Я также прочитал приложение C4 о совместимости между C ++ 14 и текущими стандартами (C ++ 17). Удаление возможности использовать арифметику указателей между объектами, динамически создаваемыми в едином символьном массиве, было бы важным изменением, на которое следует упомянуть IMHO, потому что это часто используемая функция. Поскольку на страницах совместимости ничего об этом не содержится, я думаю, что это подтверждает, что стандарт не имел намерения запрещать это.
В частности, это нарушило бы обычное динамическое построение массива объектов из класса без конструктора по умолчанию:
class T {
...
public T(U initialization) {
...
}
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem);
for (i=0; i<N; i++) {
U u(...);
new(arr + i) T(u);
}
arr
затем можно использовать как указатель на первый элемент массива ...