Страницы

Поиск по вопросам

пятница, 12 июля 2019 г.

Вопрос про касты переменных и указателей в C

Здравствуйте.
Этот вопрос основан на решении @VladD из моего предыдущего вопроса: Возможен ли доступ к элементам struct без прямого обращения к ним через точку?. В нижеследующем листинге это решение - функция MKMapPointGetCoordinateForAxis1()
Сегодня я добавил в своём проекте флаг -Weverything, в результате чего появился такой вот варнинг:
Cast from 'char *' to 'double *' increases required alignment from 1 to 4
Я пошёл искать решение этой проблемы и нашёл 2 работающих решения, которые вместо char * делают касты соответственно в uintptr_t или void * - в моём листинге это соответственно функции MKMapPointGetCoordinateForAxis2 и MKMapPointGetCoordinateForAxis3
Чего я не понимаю, так это того, почему эти решения работают, а решения, например, с тоже 8-байтовыми double * и, например, long long * (MKMapPointGetCoordinateForAxis4 и MKMapPointGetCoordinateForAxis5) - нет.
Мой вопрос явно как-то связан с механикой кастов в С, а именно, что на самом деле происходит при следующих видах кастов:
*(double *)((char *)point + MKMapPointOffsets[axis]) *(double *)((uintptr_t)point + MKMapPointOffsets[axis]) *(double *)((void *)point + MKMapPointOffsets[axis]) *(double *)((double *)point + MKMapPointOffsets[axis]) *(double *)((long long *)point + MKMapPointOffsets[axis])
то есть, какова механика кастов во всех этих пяти случаях?
И ещё вопрос: почему работает uintptr_t, ведь на моей системе за ним стоит обычный unsigned long (даже не указатель, а просто числовой тип).
P.S. В начале я думал, что дело в размере типов/указателей, отсюда и моё предположение, что должны работать double * и long long * (ведь void * же работает!), но теперь я чётко вижу, что тут дело в чём-то ещё кроме размера указателей, о чём и прошу разъяснения. Мне кажется, что ответ на этот вопрос, несмотря на специфичность его формулировки и листинга, поможет мне прояснить какие-то очень важные вещи про касты и типы в языке С.

Листинг
#import
typedef struct { double x; double y; } MKMapPoint;
static const size_t MKMapPointXOffset = offsetof(MKMapPoint, x); static const size_t MKMapPointYOffset = offsetof(MKMapPoint, y); static const size_t MKMapPointOffsets[] = { MKMapPointXOffset, MKMapPointYOffset };
static inline double MKMapPointGetCoordinateForAxis1(MKMapPoint *point, int axis) { return *(double *)((char *)point + MKMapPointOffsets[axis]); } static inline double MKMapPointGetCoordinateForAxis2(MKMapPoint *point, int axis) { return *(double *)((uintptr_t)point + MKMapPointOffsets[axis]); } static inline double MKMapPointGetCoordinateForAxis3(MKMapPoint *point, int axis) { return *(double *)((void *)point + MKMapPointOffsets[axis]); } static inline double MKMapPointGetCoordinateForAxis4(MKMapPoint *point, int axis) { return *(double *)((double *)point + MKMapPointOffsets[axis]); } static inline double MKMapPointGetCoordinateForAxis5(MKMapPoint *point, int axis) { return *(double *)((long long *)point + MKMapPointOffsets[axis]); }
int main(int argc, const char * argv[]) {
printf("%ld
", sizeof(MKMapPoint)); printf("%ld %ld
", MKMapPointXOffset, MKMapPointYOffset);
printf("%ld %ld %ld %ld %d
", sizeof(char *), sizeof(uintptr_t), sizeof(void *), sizeof(double *), sizeof(long long *));
MKMapPoint x = (MKMapPoint){111, 222};
printf("%f %f
", MKMapPointGetCoordinateForAxis1(&x, 0), MKMapPointGetCoordinateForAxis1(&x, 1)); printf("%f %f
", MKMapPointGetCoordinateForAxis2(&x, 0), MKMapPointGetCoordinateForAxis2(&x, 1)); printf("%f %f
", MKMapPointGetCoordinateForAxis3(&x, 0), MKMapPointGetCoordinateForAxis3(&x, 1)); printf("%f %f
", MKMapPointGetCoordinateForAxis4(&x, 0), MKMapPointGetCoordinateForAxis4(&x, 1)); printf("%f %f
", MKMapPointGetCoordinateForAxis5(&x, 0), MKMapPointGetCoordinateForAxis5(&x, 1));
return 0; }
16 0 8 8 8 8 8 8 111.000000 222.000000 111.000000 222.000000 111.000000 222.000000 111.000000 0.000000 111.000000 0.000000 Program ended with exit code: 0


Ответ

Я прочитал несколько текстов и сделал несколько тестов и теперь, похоже, могу более или менее корректно ответить на все свои вопросы.
Как выяснилось, правильным и стабильным является ответ, который использует uintptr_t
*(double *)((uintptr_t)point + MKMapPointOffsets[axis])
Причины этого описаны подробно ниже в моих ответах на собственные вопросы.

Чего я не понимаю, так это того, почему эти решения работают, а решения, например, с тоже 8-байтовыми double * и, например, long long * (MKMapPointGetCoordinateForAxis4 и MKMapPointGetCoordinateForAxis5) - нет.
Если кратко: то неработающие решения не работают потому, что их размер их типов - 8 (не указателей, а именно самих типов!), и потому за операцией + MKMapPointOffsets[axis] на самом деле скрывается + MKMapPointOffsets[axis] * sizeof(double или long long), то есть MKMapPointOffsets[axis] * 8, что каждый раз выбрасывает эти неработающие решения за границы структуры.
Более развёрнуто:
Действительно, как правильно указал @KoVadim:
(тип *)указатель+смещение компилятор преобразует это в указатель + смещение * sizeof(тип)
В целом, я так и понимал эту арифметику, просто почему-то именно в этом случае мне ошибочно казалось, что должно быть sizeof(тип *), откуда и возникало недоумение по поводу того, что double * и long long * не работают, ведь у них sizeof равен 8 также как и у uintptr_t и `char *
После того, как я уловил эту свою ошибку, всё встало на свои места:
Вот размеры структуры и соответствующих отступов для x и y координат:
printf("%ld
", sizeof(MKMapPoint)); printf("%ld %ld
", MKMapPointXOffset, MKMapPointYOffset);
16 0 8
То есть x имеет смещение 0 относительно указателя на структуру, а y соответственно - 8
Посмотрим ещё раз на исходное решение @VladD:
return *(double *)((char *)point + MKMapPointOffsets[axis]);
Я только сегодня окончательно разобрался с тем, зачем тут нужен каст в (char *): а именно он нужен для того, чтобы при прибавлении отступа он умножался на sizeof(char), то есть единицу. В результате мы и получаем: "указатель на структуру + отступ-соответствующей-координаты".

Почему работает void *?
На самом деле решение с void * нельзя считать подходящим, так как оно выдаёт другой варнинг:
Arithmetic on a pointer to void is a GNU extension
но для понимания оказалось важно, что работает оно также, как и char * по той причине, что в случае, когда это extension доступно, то sizeof(void) равняется 1.
Об этом очень интересный комментарий здесь: sizeof(void) equals 1 in C? [duplicate]
Откуда опять получается, что за выражением
return *(double *)((void *)point + MKMapPointOffsets[axis]);
стоит (это уже сейчас будет псевдокод)
return *(double *)((void *)point + MKMapPointOffsets[axis] * sizeof(void));
то есть мы снова получаем отступ 0 для х и 8 для y

И ещё вопрос: почему работает uintptr_t, ведь на моей системе за ним стоит обычный unsigned long (даже не указатель, а просто числовой тип).
Ответить на этот вопрос мне оказалось интереснее всего, потому что это решение отличается от остальных рабочих: с char * и с void *
Это решение не использует адресную арифметику для работы с указателями, оно просто берёт указатель на начало структуры, превращает его в число и добавляет к этому числу 0 или 8 в зависимости от x это или y:
return *(double *)((uintptr_t)point + MKMapPointOffsets[axis]);
То есть в данном случае происходит обычное сложение двух чисел (нет умножения на sizeof, как это было бы в случае с указателями).
Это возможно потому, что uintptr_t обладает замечательным свойством (What is uintptr_t data type):
In C99, it is defined as "an unsigned integer type with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer".
То есть, другими словами, можно быть уверенными в том, что мы делаем каст адреса в число, которое имеет тот же размер, что и размер этого адреса.

Ниже следует перечень тестов, которые навели меня на верные мысли (надеюсь, я нигде не ошибся).
И ещё позже я нашёл интересный топик на другую тему, но в котором тоже очень интересным образом используется uintptr_t: Solve the memory alignment in C interview question that stumped me
P.S. Спасибо @KoVadim и @avp за ценные интересные комментарии как к вопросу, так и к ответу.

Тесты и логи:
printf("
Testing offsets:

");
printf("%ld %ld
", sizeof(char), sizeof(void));
printf("%p
", (&x)); printf("%p
", (&x + 1));
printf("%p
", ((char *)&x)); printf("%p
", ((char *)&x + 1));
printf("%p
", ((void *)&x)); printf("%p
", ((void *)&x + 1));
printf("%p
", ((uintptr_t)&x)); printf("%p
", ((uintptr_t)&x + 1));
uintptr_t ptr = (uintptr_t)&x;
printf("%p %p %p
", &x, ptr, (void *)ptr);
printf("%lu
", (&x)); printf("%lu
", ((uintptr_t)&x));
Testing offsets:
1 1 0x7fff5fbffa10 0x7fff5fbffa20 0x7fff5fbffa10 0x7fff5fbffa11 0x7fff5fbffa10 0x7fff5fbffa11 0x7fff5fbffa10 0x7fff5fbffa11 0x7fff5fbffa10 0x7fff5fbffa10 0x7fff5fbffa10 140734799804944 140734799804944

Комментариев нет:

Отправить комментарий