Всем привет, недавно у меня стало возникать слишком много мыслей о том, насколько легитимно приводить типы указателей в некоторых случаях и не нарушает ли это правил strict aliasing.
Компилируется код такой командой (gcc 4.9.2):
gcc test.c -o /dev/null -O3 -Wall -Wextra
Случай №1. Есть какой-то буфер в виде массива чаров, полученный откуда-то (по сети, например). Хочется его распарсить, для этого привести char * к какому-нибудь struct payload * и работать со структурой; выравнивание и порядок байтов к вопросу отношения не имеют, считаем, что там всё правильно. Для примера можно для упрощения вместо struct payload взять обычный int — с ним происходит то же самое:
int main()
{
char buf[5] = "TEST";
int *p = (int *)&buf; // По стандарту char может алиасить любой тип, но не наоборот
*p = 0x48414559; // Но здесь предупреждения о нарушении strict aliasing почему-то нет
*(int *)buf = 0x48414559; // А вот здесь есть
// *(int *)(buf+1) = 0x48414559; // Вот так уже не будет, кстати
return 0;
}
test.c: In function 'main':
test.c:6:2: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
*(int *)buf = 0x48414559;
^
Вопрос: чем отличается доступ через указатель p и через buf, приведенный к int *? Почему в одном случае нет варнинга, в другом есть? Действительно ли в одном случае нарушается правило strict aliasing, а в другом нет? Или это потерянный варнинг? Или особенность реализации gcc?
Случай №2. Приведение struct sockaddr_in * к struct sockaddr *, использующееся повсеместно. Для чистоты эксперимента структуры объявнены вручную, а не взяты из хедеров. Да, их наполнение отличается от того, что там должно быть. Итак, я решил продолжить эксперимент с приведением типа указателя без промежуточной переменной.
#include <stdint.h>
struct sockaddr {
uint16_t sa_family;
char sa_data[14];
};
struct sockaddr_in
{
uint16_t sin_family;
uint16_t sin_port;
uint32_t sin_addr;
char sin_zero[8];
};
int main()
{
{
struct sockaddr_in addr;
((struct sockaddr *)&addr)->sa_family = 2; // Тут варнинга почему-то нет
}
{
char addr[16];
((struct sockaddr *)&addr)->sa_family = 2; // А тут есть, как и в предыдущем примере
}
{
uint32_t addr[4];
((struct sockaddr *)&addr)->sa_family = 2; // А здесь почему-то снова нет
}
return 0;
}
Господа, я в замешательстве. Вот моё мнение по этому поводу:
В первом примере нарушения правила strict aliasing есть в обоих случаях (char может алиасить любой тип, но не наоборот), однако варнинг есть почему-то в одном из случаев, в связи с этим вопрос: это недостающий варнинг или особенность поведения gcc?
Во втором примере нарушений правила strict aliasing нет, поскольку я обращаюсь только к объекту struct sockaddr. Однако в случае, когда addr — это массив чаров (как в первом примере), варнинг возникает. Здесь аналогичный вопрос: это лишний варнинг, или же смысл различен?
Ну и один глобальный вопрос: если я где-то в своих рассуждениях ошибаюсь (или чего-то не понимаю), то где?