Разыменование указателя типа наказанного нарушит правила строгого наложения имен - PullRequest
45 голосов
/ 14 июля 2010

Я использовал следующий фрагмент кода для чтения данных из файлов как часть более крупной программы.

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, я нашел два ортогональных ответа:

против

В конце концов, я не хочу игнорировать предупреждения. Что бы вы порекомендовали?

[обновление] Я заменил пример игрушки на реальную функцию.

Ответы [ 7 ]

39 голосов
/ 12 октября 2012

Проблема возникает из-за того, что вы обращаетесь к массиву символов через 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;

Строгое предупреждение псевдонимов действительно важно понять или исправить.Они вызывают ошибки, которые невозможно воспроизвести собственными силами, поскольку они возникают только на одном конкретном компиляторе в одной конкретной операционной системе на одной конкретной машине и только в полнолуние и один раз в год и т. Д.

26 голосов
/ 14 июля 2010

Это выглядит так, как будто вы действительно хотите использовать fread:

int data;
fread(&data, sizeof(data), 1, stream);

Тем не менее, если вы хотите пойти по пути чтения символов, а затем интерпретировать их как int, безопасный способ сделать это в C (но не в 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 байта.

Обратите внимание, что и ваш код, и мой очень непереносимы.

Редактировать: если вы хотите прочитать целые числа различной длины из файла, переносите, попробуйте что-то вроде этого:

unsigned result=0;
for(int i=0; i<4; i++)
    result = (result << 8) | fgetc(stream);

(Примечание: в реальной программе вы бы дополнительно хотели проверить возвращаемое значение fgetc () по сравнению с EOF.)

Это читает 4-байтовый неподписанный из файла в формате с прямым порядком байтов, независимо от того, что такое порядковый номер системы. Он должен работать практически на любой системе, где значение без знака составляет не менее 4 байтов.

Если вы хотите быть порядком байтов, не используйте указатели или союзы; используйте вместо этого битовые сдвиги.

7 голосов
/ 28 апреля 2016

Этот документ обобщает ситуацию: http://dbp -consulting.com / tutorials / StrictAliasing.html

Существует несколько различных решений, но наиболее переносимым / безопаснымиспользуйте memcpy ().(Вызовы функций могут быть оптимизированы, поэтому это не так неэффективно, как кажется.) Например, замените это:

return *(short*)data;

этим:

short temp;
memcpy(&temp, data, sizeof(temp));
return temp;
7 голосов
/ 22 декабря 2010

Использование объединения не правильная вещь, которую нужно сделать здесь.Чтение из неписаного члена объединения не определено, т. Е. Компилятор может выполнять оптимизацию, которая нарушит ваш код (например, оптимизировать удаление записи).

2 голосов
/ 14 июля 2010

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

Преобразование трехбайтового массива символов в int - одна из худших вещей, которые я когда-либо видел. Обычно ваш int имеет как минимум 4 байта. Таким образом, для четвертого (а может и больше, если int шире) вы получаете случайные данные. И затем вы бросаете все это в double.

Просто не делай этого. Проблема с псевдонимами, о которой предупреждает gcc, невинна по сравнению с тем, что вы делаете.

0 голосов
/ 14 апреля 2016

Авторы Стандарта C хотели позволить авторам компилятора генерировать эффективный код в обстоятельствах, когда это было бы теоретически возможно, но маловероятно, что глобальная переменная могла бы получить доступ к своему значению, используя, казалось бы, не связанный указатель.Идея состояла не в том, чтобы запретить наложение типов путем приведения и разыменования указателя в одном выражении, а в том, чтобы сказать, что с учетом чего-то вроде:

int x;
int foo(double *d)
{
  x++;
  *d=1234;
  return x;
}

компилятор будет иметь право предполагать, что запись в *д не повлияет на х.Авторы Стандарта хотели перечислить ситуации, в которых функция, подобная вышеприведенной, которая получает указатель из неизвестного источника, должна была бы предположить, что она могла бы создать псевдоним, казалось бы, не связанной глобальной, не требуя, чтобы типы полностью соответствовали.К сожалению, в то время как обоснование настоятельно рекомендует, чтобы авторы Стандарта намеревались описать стандарт минимального соответствия в случаях, когда у компилятора в противном случае не было бы оснований полагать, что вещи могут иметь псевдоним , правило не требует, чтобы компиляторыраспознавать псевдонимы в тех случаях, когда это очевидно , и авторы gcc решили, что они скорее сгенерируют наименьшую возможную программу, соответствующую плохо написанному языку стандарта, чем сгенерируют код, который на самом делеполезно, и вместо того, чтобы распознавать псевдонимы в тех случаях, когда это очевидно (хотя все еще можно предположить, что вещи, которые не выглядят как псевдонимы, не будут), они скорее потребуют, чтобы программисты использовали memcpy, таким образом, требуякомпилятор, позволяющий предположить, что указатели неизвестного происхождения могут иметь псевдоним практически для чего угодно, что препятствует оптимизации.

0 голосов
/ 16 августа 2010

Очевидно, что стандарт позволяет sizeof (char *) отличаться от sizeof (int *), поэтому gcc жалуется при попытке прямого приведения.Пустота * немного особенная в том, что все может быть преобразовано туда и обратно в пустоту *.На практике я не знаю многих архитектур / компиляторов, в которых указатель не всегда одинаков для всех типов, но gcc вправе выдавать предупреждение, даже если оно раздражает.

Я думаю, что безопасный способ был бы

int i, *p = &i;
char *q = (char*)&p[0];

или

char *q = (char*)(void*)p;

Вы также можете попробовать это и посмотреть, что вы получите:

char *q = reinterpret_cast<char*>(p);
...