Является ли указатель с правильным адресом и типом все еще всегда действительным указателем с С++ 17?

(В отношении этот вопрос и ответ.)

До стандарта С++ 17 следующее предложение было включено в [basic.compound]/3:

Если объект типа T расположен по адресу A, указатель типа cv T *, значение которого является адресом A, называется указывать на этот объект, независимо от того, как это значение было получено.

Но поскольку С++ 17, это предложение было удалено.

Например, я считаю, что это предложение определило этот пример кода и что поскольку С++ 17 это поведение undefined:

 alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;

До С++ 17 p1+1 содержит адрес *p2 и имеет правильный тип, поэтому *(p1+1) является указателем на *p2. В С++ 17 p1+1 является указателем past-the-end, поэтому он не является указателем на объект, и я считаю, что это не разыменовываемый.

Является ли эта интерпретация этой модификации стандартного права или существуют другие правила, которые компенсируют удаление цитируемого предложения?

Ответ 1

Является ли эта интерпретация этой модификации стандартного права или существуют другие правила, которые компенсируют удаление этого цитируемого предложения?

Да, эта интерпретация верна. Указатель мимо конца не просто конвертируется в другое значение указателя, которое указывает на этот адрес.

В новом [basic.compound]/3 говорится:

Каждое значение типа указателя является одним из следующих:
(3.1) указатель на объект или функцию (указатель, как говорят, указывает на объект или функцию), или
(3.2) указатель мимо конца объекта ([expr.add]) или

Это взаимоисключающие. p1+1 - это указатель за концом, а не указатель на объект. p1+1 указывает на гипотетический x[1] массив size-1 в p1, а не на p2. Эти два объекта не являются взаимно конвертируемыми.

Мы также имеем ненормативную ноту:

[Примечание. Указатель за конец объекта ([expr.add]) не считается указывающим на несвязанный объект типа объекта, который может быть расположен по этому адресу. [...]

который уточняет намерение.


Как T.C. указывает на многочисленные комментарии (особенно этот), это действительно особый случай проблемы, возникающей при попытке реализовать std::vector - это то, что [v.data(), v.data() + v.size()) должен быть допустимым диапазоном, и тем не менее vector не создает объект массива, поэтому только определенная арифметика указателя будет проходить от любого заданного объекта в векторе до конца его гипотетического одномерного массива. Больше ресурсов, см. CWG 2182, это обсуждение std, и два пересмотра статьи по теме: P0593R0 и P0593R1 (раздел 1.3).

Ответ 2

В вашем примере *(p1 + 1) = 10; должен быть UB, потому что он один за концом массива размера 1. Но мы здесь в очень особенном случае, потому что массив был динамически построен в более крупном char массив.

Создание динамического объекта описано в 4.5. Объектная модель С++ [intro.object], § 3 проекта n4659 стандарта С++:

3 Если создается полный объект (8.3.4) в хранилище, связанный с другим объектом e типа "массив из N unsigned char" или массива типа 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; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)

Итак, в примере массив 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 для С++ 17 - [basic.coumpound] §3 (подчеркните мой):

3... Каждый Значение типа указателя является одним из следующих:
(3.1) - указатель на объект или функцию (указатель, как говорят, указывает на объект или функцию), или
(3.2) - указатель мимо конца объекта (8.7) или
(3.3) - значение нулевого указателя (7.11) для этого типа или
(3.4) - недопустимое значение указателя.

Значение типа указателя, которое является указателем на конец объекта или мимо него, представляет собой адрес первый байт в памяти (4.4), занятый объектом или первым байтом в памяти после окончания хранениязанимаемых объектом, соответственно. [Примечание: указатель мимо конца объекта (8.7) не рассматривается укажите несвязанный объект типа объектов, который может быть расположен по этому адресу. Значение указателя становится недействительным, когда память, которую он обозначает, доходит до конца срока хранения; см. 6.7. -end note] Для целей арифметики указателя (8.7) и сравнения (8.9, 8.10) указатель проходит мимо конца последнего элемента из массива x из n элементов считается эквивалентным указателю на гипотетический элемент x [n]. представление значений типов указателей определяется реализацией. Указатели на совместимые с макетами типы должны имеют одинаковые требования к представлению и выравниванию значений (6.11)...

Примечание. Указатель мимо конца... здесь не применяется, потому что объекты, на которые указывают p1 и p2, а не несвязанные, но вложены в один и тот же полный объект, так что арифметика указателя имеет смысл внутри объект, обеспечивающий хранение: p2 - p1 определяется и (&buffer[sizeof(int)] - buffer]) / sizeof(int), который равен 1.

Итак p1 + 1 есть указатель на *p2, а *(p1 + 1) = 10; определил поведение и установил значение *p2.


Я также прочитал приложение C4 о совместимости между стандартами С++ 14 и current (С++ 17). Удаление возможности использования арифметики указателя между объектами, динамически создаваемыми в одном массиве символов, было бы важным изменением, которое следует указывать в IMHO, потому что это обычно используемая функция. Поскольку ничего не существует на страницах совместимости, я думаю, что это подтверждает, что это не означает, что стандарт запрещает это.

В частности, это приведет к поражению этой общей динамической конструкции массива объектов из класса без конструктора по умолчанию:

class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}

arr может затем использоваться как указатель на первый элемент массива...

Ответ 3

Чтобы расширить приведенные ниже ответы, я приведу пример того, что, по моему мнению, пересмотренная формулировка исключает:

Предупреждение: Undefined Поведение

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

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

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

Этот вывод показывает, что в этом случае хранятся два массива (в этом случае), так что "один за конец" A происходит, чтобы удерживать значение адреса первого элемента B.

Пересмотренная спецификация гарантирует, что независимо A+1 никогда не будет действительным указателем на B. Старая фраза "независимо от того, как получается значение" говорит, что если "A + 1" указывает на "B [0]", то это действительный указатель на "B [0]". Это не может быть хорошим и, конечно же, никогда не намерением.