Преобразования между указателем-к-Т массив-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 ответа:
Это ответ только на 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 не запускается при доступе к данным через преобразованные указатели.