Псевдонимы, вызываемые разыменованием, нарушат правила строгого сглаживания
Я использовал следующий фрагмент кода для чтения данных из файлов как часть более крупной программы.
double data_read(FILE *stream,int code) {
char data[8];
switch(code) {
case 0x08:
return (unsigned char)fgetc(stream);
case 0x09:
return (signed char)fgetc(stream);
case 0x0b:
data[1] = fgetc(stream);
data[0] = fgetc(stream);
return *(short*)data;
case 0x0c:
for(int i=3;i>=0;i--)
data[i] = fgetc(stream);
return *(int*)data;
case 0x0d:
for(int i=3;i>=0;i--)
data[i] = fgetc(stream);
return *(float*)data;
case 0x0e:
for(int i=7;i>=0;i--)
data[i] = fgetc(stream);
return *(double*)data;
}
die("data read failed");
return 1;
}
Теперь мне говорят использовать -O2
и я получаю следующее предупреждение gcc: warning: dereferencing type-punned pointer will break strict-aliasing rules
Google, я нашел два ортогональных ответа:
против
В конце концов, я не хочу игнорировать предупреждения. Чтобы вы посоветовали?
[update] Я заменил игрушечный пример реальной функцией.
Ответы
Ответ 1
Это выглядит так, как будто вы действительно хотите использовать fread:
int data;
fread(&data, sizeof(data), 1, stream);
Тем не менее, если вы хотите пойти по пути чтения символов, а затем переинтерпретировать их как int, безопасный способ сделать это в C (но не на С++) - использовать союз:
union
{
char theChars[4];
int theInt;
} myunion;
for(int i=0; i<4; i++)
myunion.theChars[i] = fgetc(stream);
return myunion.theInt;
Я не уверен, почему длина data
в вашем исходном коде равна 3. Предполагаю, что вам нужно 4 байта; по крайней мере, я не знаю никаких систем, где int - 3 байта.
Обратите внимание, что и ваш код, и мой очень не переносимы.
Изменить: Если вы хотите прочитать ints различной длины из файла, переносимо, попробуйте что-то вроде этого:
unsigned result=0;
for(int i=0; i<4; i++)
result = (result << 8) | fgetc(stream);
(Примечание: в реальной программе вы также захотите проверить возвращаемое значение fgetc() на EOF.)
Это считывает 4-байтовое без знака из файла в формате little-endian, независимо от того, что такое консистенция системы. Он должен работать практически с любой системой, где unsigned имеет не менее 4 байтов.
Если вы хотите быть нейтральным по отношению к конечному, не используйте указатели или союзы; вместо этого используйте бит-сдвиги.
Ответ 2
Проблема возникает из-за того, что вы получаете доступ к char -array через double*
:
char data[8];
...
return *(double*)data;
Но gcc предполагает, что ваша программа никогда не будет обращаться к переменным, хотя указатели различного типа. Это предположение называется строгим сглаживанием и позволяет компилятору сделать некоторые оптимизации:
Если компилятор знает, что ваш *(double*)
никоим образом не перекрывается с data[]
, он допускает всевозможные вещи, такие как переупорядочение вашего кода:
return *(double*)data;
for(int i=7;i>=0;i--)
data[i] = fgetc(stream);
Скот, скорее всего, оптимизирован, и вы получите просто:
return *(double*)data;
Что оставляет ваши данные [] неинициализированными. В этом конкретном случае компилятор может увидеть, что ваши указатели перекрываются, но если вы объявили его char* data
, он мог бы дать ошибки.
Но правило строгого сглаживания гласит, что char * и void * могут указывать на любой тип. Поэтому вы можете переписать его на:
double data;
...
*(((char*)&data) + i) = fgetc(stream);
...
return data;
Предупреждения строгого сглаживания действительно важны для понимания или исправления. Они вызывают ошибки, которые невозможно воспроизвести внутри компании, поскольку они происходят только на одном конкретном компиляторе в одной конкретной операционной системе на одной конкретной машине и только на полнолуние и один раз в год и т.д.
Ответ 3
Использование объединения - это не то, что нужно сделать здесь. Чтение из неписаного члена союза undefined - то есть компилятор может свободно выполнять оптимизацию, которая приведет к нарушению вашего кода (например, оптимизация записи).
Ответ 4
В этом документе суммируется ситуация: http://dbp-consulting.com/tutorials/StrictAliasing.html
Существует несколько различных решений, но наиболее переносимым/безопасным является использование memcpy(). (Вызов функций может быть оптимизирован, поэтому он не так неэффективен, как кажется.) Например, замените это:
return *(short*)data;
С этим:
short temp;
memcpy(&temp, data, sizeof(temp));
return temp;
Ответ 5
В принципе, вы можете прочитать сообщение gcc как парень, которого вы ищете, не говорите, что я не предупреждал вас.
Передача трехбайтового символьного массива в int
является одной из худших вещей, которые я видел, когда-либо. Обычно ваш int
имеет как минимум 4 байта. Таким образом, для четвертого (и, возможно, большего, если int
шире) вы получаете случайные данные. И затем вы отбросили все это до double
.
Просто не делай этого. Проблема сглаживания, о которой предупреждает gcc, невинна по сравнению с тем, что вы делаете.
Ответ 6
Авторы C-стандарта хотели, чтобы авторы компилятора генерировали эффективный код в обстоятельствах, когда это было бы теоретически возможно, но маловероятно, чтобы глобальная переменная могла иметь доступ к этому значению с использованием, казалось бы, несвязанного указателя. Идея заключалась не в том, чтобы запретить тип punning путем кастинга и разыменования указателя в одном выражении, а скорее сказать, что что-то вроде:
int x;
int foo(double *d)
{
x++;
*d=1234;
return x;
}
компилятор будет иметь право предположить, что запись в * d не повлияет на x. Авторы Стандарта хотели перечислить ситуации, в которых функция, подобная приведенной выше, получившая указатель из неизвестного источника, должна была бы предположить, что она может быть псевдонимом по-видимому несвязанным глобальным, не требуя, чтобы эти типы идеально соответствовали. К сожалению, хотя обоснование настоятельно предлагает, чтобы авторы Стандарта планировали описать стандарт для минимального соответствия в случаях, когда у компилятора в противном случае не было бы оснований полагать, что все может быть псевдонимом, правило не требует, чтобы компиляторы распознавали псевдонимы в тех случаях, когда это очевидно, и авторы gcc решили, что они скорее сгенерируют самую маленькую программу, которая она может, в то время как она соответствует плохо написанному языку Стандарта, чем генерирует действительно полезный код и вместо того, чтобы распознавать псевдонимы в случаях, когда это очевидно (хотя они все еще могут предполагать, что вещи, которые не похожи на псевдонимы, не будут), они предпочли бы, чтобы программисты использовали memcpy
, что требует от компилятора возможности для указания указателей неизвестного происхождения может быть псевдоним практически любого, что препятствует оптимизации.
Ответ 7
По-видимому, стандарт позволяет sizeof (char *) отличаться от sizeof (int *), поэтому gcc жалуется при попытке прямого трансляции. void * является немного особенным в том, что все может быть преобразовано обратно и вперед в и из void *.
На практике я не знаю много архитектуры/компилятора, где указатель не всегда одинаковый для всех типов, но gcc прав, чтобы выпустить предупреждение, даже если это раздражает.
Я думаю, что безопасным способом было бы
int i, *p = &i;
char *q = (char*)&p[0];
или
char *q = (char*)(void*)p;
Вы также можете попробовать это и посмотреть, что получите:
char *q = reinterpret_cast<char*>(p);