Разыменование одного за концом указателя на тип массива - PullRequest
0 голосов
/ 09 октября 2018

Хорошо ли определено в c ++ для разыменования указателя типа «один за другим» на тип массива?

Рассмотрим следующий код:

#include <cassert>
#include <iterator>

int main()
{
    // An array of ints
    int my_array[] = { 1, 2, 3 };

    // Pointer to the array
    using array_ptr_t = int(*)[3];
    array_ptr_t my_array_ptr = &my_array;

    // Pointer one-past-the-end of the array
    array_ptr_t my_past_end = my_array_ptr + 1;

    // Is this valid?
    auto is_this_valid = *my_past_end;

    // Seems to yield one-past-the-end of my_array
    assert(is_this_valid == std::end(my_array));
}

Общепринято, что этонеопределенное поведение для разыменования указателя «один за другим».Однако верно ли это для указателей на типы массивов?

Кажется разумным, что это должно быть допустимо, поскольку *my_past_end может быть решено исключительно с помощью арифметики указателей и дает указатель на первый элемент в массиве, который будет , что также является действительным «101» * «исходящий конец» для исходного массива my_array.

Однако другой способ взглянуть на это состоит в том, что*my_past_end создает ссылку на несуществующий массив, который неявно преобразуется в int*.Эта ссылка кажется мне проблематичной.

Для контекста, мой вопрос был вызван этим вопросом , в частности комментариями к этому ответу .

Изменить: Этот вопрос не является дубликатом Возьмите адрес элемента массива один за другим через нижний индекс: законно по стандарту C ++ или нет? Я спрашиваю, объясняется ли правило, описанное вВопрос также относится к указателям, указывающим на тип массива.

Редактировать 2: Удалено auto, чтобы сделать явным, что my_array_ptr не является int*.

Ответы [ 3 ]

0 голосов
/ 09 октября 2018

Я считаю, что он хорошо определен, потому что не разыменовывает указатель "один за другим".

auto is_this_valid = *my_past_end;

my_past_end имеет тип int(*)[3] (указатель на массив из 3 int элементов).Следовательно, выражение *my_past_end имеет тип int[3] - поэтому, как и любое выражение массива в этом контексте, оно «затухает» до указателя типа int*, указывающего на начальный (нулевой) элемент объекта массива.Этот «распад» является операцией времени компиляции.Таким образом, инициализация просто инициализирует is_this_valid, указатель типа int*, чтобы указывать сразу после конца my_array.Нет доступа к памяти за концом объекта массива.

0 голосов
/ 21 марта 2019

Стандарт, похоже, предполагает, что это не неопределенное поведение.

Соответствующий раздел стандарта выглядит следующим образом (о результате добавления типа указателя к целочисленному типу илинаоборот)

§5.7p4 [expr.add]

Когда выражение с целым типом добавляется или вычитается из указателя, результат имеет типоперанд указателя.Если операнд-указатель указывает на элемент объекта массива 84 , и массив достаточно велик, результат указывает на смещение элемента от исходного элемента, так что различие индексов полученного и исходногоэлементы массива равны интегральному выражению.[...] выражение (P)+1 указывает один за последним элементом объекта массива.[...] Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или один после последнего элемента объекта массива, вычисление не должно производиться и переполняться;в противном случае поведение не определено.

С сноской 84:

С этой целью считается, что объект, который не является элементом массива, относится к одному элементному массиву;см. 5.3.1

(И §5.3.1 составляет около & и *)

Итак, для целей, где my_array_ptr и my_past_end указываютони указывают на my_array, как если бы my_array было на самом деле int[1][3].my_array_ptr указывает на первый элемент (int[3], которым на самом деле является my_array).my_past_end указывает на элемент «один за другим», и это хорошо определено.

Когда вы делаете *my_past_end, вы создаете lvalue для int[3].Пока это не преобразуется в значение prvalue, вы фактически не обращаетесь к памяти, которая не является int[3], как если бы она была int[3].

§3.9.2p1 [basic.compound]

Составные типы можно создавать следующими способами:
[...]
4. ссылается на объекты или функции данного типа

§3.9.2p3 [basic.compound]

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

Обратите внимание, что он очень старается, чтобы убедиться, что-end-pointer по-прежнему определяется как адрес объекта.Поскольку ссылки могут ссылаться только на объекты, это допускает «недопустимые» ссылки, такие как * (указатель конца конца), но по-прежнему запрещает нулевые ссылки, так как nullptr не указывает на объект.

§4.2p1 [conv.array]

Значение l или значение типа "массив из N T" или "массив с неизвестной границей T" можно преобразовать вprvalue типа "указатель на T".Результатом является указатель на первый элемент массива.

Поскольку lvalue преобразуется, доступ к недопустимой памяти невозможен.Таким образом, во время преобразования создается значение типа int*, указывающее на тот же адрес, что и &my_array[3], то есть значение std::end(my_array).Таким образом, они будут равны (что неудивительно, что указатели, указывающие на один и тот же адрес, определены как равные)

Вы также можете преобразовать my_past_end в int* напрямую, и это будет работать, как int[3] является составным типом int s (int является подобъектом int[3]), так что это будет менее запутанным способом сделать это.

В качестве примечания, причина, по которой &my_array[4] работает как в C, так и в C ++, хотя C не имеет ссылок, потому что my_array[4] определен как *(my_array + 4), а &my_array[4] равен &*(my_array + 4), а &*(expression) в C такой же, какпреобразование (expression) в значение (и эффективное утверждение, что это ненулевой указатель).Поскольку в C ++ такого исключения не существует, здесь показана логика (my_array[4] - это ссылка, которую нельзя преобразовать в значение).


Это выглядит довольно неоднозначно.«Несвязанные» объекты никогда больше не упоминаются в стандарте.Они могут быть заняты чем-то другим, например:

int arr[3][3] = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

arr[0][3] и arr[1][0] указывают на один и тот же адрес памяти.Но означает ли arr[0][3] = 10;, что arr[1][0] будет обновлено, чтобы прочитать 10?

int test() {
    int arr[3][3] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };

    const int& i = arr[1][0];
    arr[0][3] = 10;
    return i;
}

Кажется, чтобы возвратить 10 в msvc и GCC (Оптимизация до mov eax, 10 ret)

Поскольку ссылка ссылается на объект с адресом, &(reference) четко определено.Но поскольку эти «несвязанные» объекты больше никогда не упоминаются, использование чего-либо, кроме их адреса, в силу буквального определения не является неопределенным поведением.

0 голосов
/ 09 октября 2018

Это CWG 232 .Может показаться, что проблема заключается в разыменовании нулевого указателя, но в основном в том, что означает просто разыменование чего-то, что не указывает на объект.В этом случае нет явного языкового правила.

Один из примеров в этом выпуске:

Аналогично, разыменование указателя на конец массива должно быть разрешено до тех пор, покапоскольку значение не используется:

char a[10];
char *b = &a[10];   // equivalent to "char *b = &*(a+10);"

Оба случая встречаются в реальном коде достаточно часто, поэтому их следует разрешить.

Это в основном то же самое, что и OP (a[10] часть вышеприведенного выражения), за исключением использования char вместо типа массива.

Общепринято, что неопределенным поведением является разыменование указателя «один за другим».Однако верно ли это для указателей на типы массивов?

Нет различий в правилах, основанных на том, какой это указатель.my_past_end - указатель конца-в-конце, поэтому, разыменовать его UB или нет, не является функцией факта, что он указывает на массив в отличие от любого другого типа.

В то время как тип is_this_valid и int*, который инициализируется из int(&)[3] (распад массива в указатель), и, таким образом, здесь ничего на самом деле не читает из памяти - это не имеет значения для работы правил языка,my_past_end - указатель, значение которого находится после конца объекта , и это единственное, что имеет значение.

...