Подтвердить что ты не робот

Псевдонимы, вызываемые разыменованием, нарушат правила строгого сглаживания

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

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] Я заменил игрушечный пример реальной функцией.

4b9b3361

Ответ 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);