Преобразования между указателем-к-Т массив-T и указатель на массив в-из-Т-либо неопределенное поведение?


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

#include <stdio.h>
int main() {
 typedef int T;
 T a[] = { 1, 2, 3, 4, 5, 6 };
 T(*pa1)[6] = (T(*)[6])a;
 T(*pa2)[3][2] = (T(*)[3][2])a;
 T(*pa3)[1][2][3] = (T(*)[1][2][3])a;
 T *p = a;
 T *p1 = *pa1;
 //T *p2 = *pa2; //error in c++
 //T *p3 = *pa3; //error in c++
 T *p2 = **pa2;
 T *p3 = ***pa3;
 printf("%p %p %p %p %p %p %pn", a, pa1, pa2, pa3, p, p1, p2, p3);
 printf("%d %d %d %d %d %d %dn", a[5], (*pa1)[5], 
   (*pa2)[2][1], (*pa3)[0][1][2], p[5], p1[5], p2[5], p3[5]);
 return 0;
}

Приведенный выше код компилируется и выполняется на языке Си, что приводит к ожидаемым результатам. Все значения указателя одинаковы, как и все значения int. Я думаю, что результат будет одинаковым для любого типа T, но int является самым простым для работы.

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

[EDIT: закомментированные строки вызывают ошибки в C++ и предупреждения в C. Я нахожу стандарт C расплывчатым в этом вопросе, но это не настоящий вопрос.]

В этом вопросе утверждалось, что это неопределенное поведение, но я его не вижу. Разве я не прав?

Кодздесь , Если вы хотите его увидеть.


Сразу после того, как я написал выше, меня осенило, что эти ошибки вызваны тем, что в C++существует только один уровень распада указателя. Больше разыменование необходимо!
 T *p2 = **pa2; //no error in c or c++
 T *p3 = ***pa3; //no error in c or c++

И прежде чем я успел закончить эту правку, @AntonSavin дал тот же ответ. Я отредактировал код, чтобы отразить эти изменения.

3 6

3 ответа:

Это ответ только на C.

C11 (n1570) 6.3.2.3 p7

Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель не выровнен правильно*) для ссылочного типа поведение не определено. В противном случае при обратном преобразовании результат сравнивается с исходным указателем.

*) В общем случае понятие "правильно выровненный" является транзитивным: если указатель на тип A правильно выровнен для указателя на Тип B, который, в свою очередь, правильно выровнен для указателя на тип C, а затем указатель на Тип A правильно выровнен для указателя на тип C.

Стандарт немного расплывчат, что произойдет, если мы используем такой указатель (строгое сглаживание в сторону) для чего-либо еще, кроме преобразования его обратно, но намерение и широко распространенная интерпретация заключается в том, что такие указатели должны сравниваться равными (и иметь одинаковое числовое значение, например, они должны также будьте равны при преобразовании в uintptr_t), в качестве примера подумайте о (void *)array == (void *)&array (преобразование в char * вместо void * явно гарантированно работает).

T(*pa1)[6] = (T(*)[6])a;

Это нормально, указатель правильно выровнен (это тот же указатель, что и &a).

T(*pa2)[3][2] = (T(*)[3][2])a; // (i)
T(*pa3)[1][2][3] = (T(*)[1][2][3])a; // (ii)

Iff T[6] имеет те же требования к выравниванию, что и T[3][2], и такие же, как T[1][2][3], (i), и (ii) безопасны, соответственно. Мне кажется странным, что они не могли, но я не могу найти гарантии в стандарте, что они должны быть те же требования к выравниванию.

T *p = a; // safe, of course
T *p1 = *pa1; // *pa1 has type T[6], after lvalue conversion it's T*, OK
T *p2 = **pa2; // **pa2 has type T[2], or T* after conversion, OK
T *p3 = ***pa3; // ***pa3, has type T[3], T* after conversion, OK

Игнорируя UB, вызванный передачей int *, где printf ожидает void *, давайте рассмотрим выражения в аргументах для следующего printf, сначала определенные:

a[5] // OK, of course
(*pa1)[5]
(*pa2)[2][1]
(*pa3)[0][1][2]
p[5] // same as a[5]
p1[5]

. отметим, что строгое сглаживание не проблема, не неправильно набрали , участвующих именующее, и мы получаем доступ к T в T.

Следующие выражения зависят от интерпретации арифметики указателей вне границ, тем более расслабленной толкование (разрешение container_of, сплющивание массива , "взлом структуры" с помощью char[] и т. д.) позволяет их также; более строгая интерпретация (допускающая надежную реализацию проверки границ времени выполнения для арифметики указателей и разыменования, но запрещающая container_of, выравнивание массива (но не обязательно "подъем" массива, что вы сделали), взлом структуры и т. д.) делает их неопределенными:

p2[5] // UB, p2 points to the first element of a T[2] array
p3[5] // UB, p3 points to the first element of a T[3] array

Единственная причина, по которой ваш код компилируется в C, заключается в том, что настройки компилятора по умолчанию позволяют компилятору неявно выполнять некоторые незаконные преобразования указателей. Формально это не допускается языком Си. Эти строки

T *p2 = *pa2;
T *p3 = *pa3;

Являются плохо сформированными в C++ и порождают нарушения ограничений В C. В обычном языке эти строки являются ошибками в обоих языках C и C++.

Любой уважающий себя компилятор C выдаст (на самом деле требуется выдать) диагностические сообщения для этих нарушений ограничений. Компилятор GCC, например, выдаст "предупреждения" о том, что типы указателей в приведенных выше инициализациях несовместимы. Хотя "предупреждений" вполне достаточно для удовлетворения стандартных требований, если вы действительно хотите использовать способность компилятора GCC распознавать ограничения, нарушающие код C, вы должны запустить его с помощью переключателя -pedantic-errors и, предпочтительно, явно выбрать версию стандартного языка с помощью переключателя -std=.

В ваш эксперимент, компилятор C выполнил эти неявные преобразования для вас как нестандартное расширение компилятора. Однако тот факт, что компилятор GCC, работающий под управлением ideone front, полностью подавил соответствующие предупреждающие сообщения (выдаваемые автономным компилятором GCC даже в его конфигурации по умолчанию), означает, что ideone является неисправным компилятором C. Его диагностический вывод не может быть осмысленно положен на то, чтобы отличить действительный код C от недействительного.

Что касается самого преобразования... Это не неопределенное поведение для выполнения этого преобразования. Но это неопределенное поведение для доступа к данным массива через преобразованные указатели.

Обновление: следующее относится только к C++ , для C прокрутите вниз. Короче говоря, нет UB в C++ и есть UB В C.

8.3.4/7 говорит:

для многомерных массивов используется непротиворечивое правило. Если E-n-мерный массив ранга i x j x ... х K, затем E, появляющееся в выражении, которое подлежит преобразованию массива в указатель (4.2), преобразуется в a указатель на (n - 1)-мерный массив с рангом j x ... икс k. Если * оператор, явный или неявный в результате подстановки, применяемой к этому указателю, получается точечный (n - 1)-мерный массив, который сам немедленно преобразуется в указатель.

Таким образом, это не приведет к ошибке в C++ (и будет работать, как ожидалось):

T *p2 = **pa2;
T *p3 = ***pa3;

Относительно того, является ли это UB или нет. Рассмотрим самое первое преобразование:

T(*pa1)[6] = (T(*)[6])a;

В C++ это фактически

T(*pa1)[6] = reinterpret_cast<T(*)[6]>(a);

И это то, что стандарт говорит о reinterpret_cast:

указатель объекта может быть явно преобразован в указатель объекта другого типа. Когда prvalue v типа "указатель на T1" преобразуется в тип "указатель на cv T2", результатом является static_cast * >(static_cast * >(v)) если и T1, и T2 являются типами стандартной компоновки (3.9) и выравнивание требования T2 не являются более строгими, чем требования T1, или если любой из типов недействителен.

Так что a преобразуется до pa1 через static_cast до void* и обратно. Статическое приведение к void* гарантированно возвращает реальный адрес a , как указано в 4.10/2:

prvalue типа "указатель на cv T", где T-тип объекта, может быть преобразовано в prvalue типа " указатель ЧВ пустоту". Результат преобразования ненулевого значения указателя типа объекта в " указатель на cv void " представляет собой адрес того же байта в памяти, что и исходное значение указателя.

Далее статическое приведение к T(*)[6] снова гарантированно возвращает тот же адрес, что и в 5.2.9/13:

значение prvalue типа "указатель на CV1 void" может быть преобразовано в значение prvalue типа "указатель на cv2 T", где T - объект, тип и cv2 такое же резюме-квалификация, или больше резюме квалификация, чем, пары CV1. Пустой указатель значение преобразуется в нулевое значение указателя типа назначения. Если исходное значение указателя представляет адреса байта в памяти и удовлетворяет требование выравнивания T, то результирующий указатель значение представляет тот же адрес, что и исходное значение указателя, то есть A

Таким образом, pa1 гарантированно указывает на тот же байт в памяти, что и a, и поэтому доступ к данным через него совершенно допустим, потому что выравнивание массивов совпадает с выравниванием базового типа.

А как насчет С?

Подумайте еще раз:

T(*pa1)[6] = (T(*)[6])a;

В стандарте C11, 6.3.2.3/7 говорится, что следующее:

указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель не выровнен правильно для ссылочного типа, поведение не определено. В противном случае, при обратном преобразовании, результат должен быть равен оригинальный указатель. Когда указатель на объект преобразуется в указатель на символьный тип, результат указывает на самый нижний адресованный байт объекта. Последовательные приращения результат, вплоть до размер объекта, выдавать указатели на оставшиеся байты объекта.

Это означает, что если преобразование не выполняется в char*, то значение преобразованного указателя не гарантируется равным значению исходного указателя, что приводит к неопределенному поведению при обращении к данным через преобразованный указатель. Чтобы заставить его работать, преобразование должно быть сделано явно через void*:
T(*pa1)[6] = (T(*)[6])(void*)a;

Преобразования обратно в T*

T *p = a;
T *p1 = *pa1;
T *p2 = **pa2;
T *p3 = ***pa3;

Все среди них есть преобразования из array of T в pointer to T, которые допустимы как в C++, так и в C, и никакой UB не запускается при доступе к данным через преобразованные указатели.