Разыменование указателя с символом типа нарушит строгие правила псевдонимов

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

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

Погуглив, я нашел два ортогональных ответа:

vs

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

[update] Я заменил игрушечный пример настоящей функцией.


person Framester    schedule 14.07.2010    source источник
comment
Ваша функция возвращает двойное значение, но вы возвращаете значение int. Почему бы не использовать удвоение?   -  person Adam Shiemke    schedule 14.07.2010
comment
Мое чтение предоставленных ссылок: ссылка bytes.com кажется в основном неправильной (на самом деле все изменилось с момента выпуска GCC 4.x), в то время как ссылка SO кажется в порядке. См. C99, 6.5 Выражения, пункт 7.   -  person Dummy00001    schedule 14.07.2010
comment
Меня немного смущает сообщение об ошибке, потому что я думал, что правила псевдонимов исключают типы char (т. е. указатель char всегда может использовать псевдоним для других указателей, если только он не restricted.) Может быть, вам нужно сделать его unsigned char для того, чтобы это применялось ..? Мне было бы интересно увидеть правильный ответ.   -  person R.. GitHub STOP HELPING ICE    schedule 14.07.2010
comment
@R Символ * может называться чем угодно, но не наоборот. Он приводит и разыменовывает char в short, int, float и double в приведенном выше коде.   -  person 5ound    schedule 14.07.2010


Ответы (7)


Похоже, вы действительно хотите использовать 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 байтов.

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

person Martin B    schedule 14.07.2010
comment
+1. Подчеркнем еще раз: объединение — это официальный способ обеспечить строгое соответствие кода алиасингу. Это не специфично для gcc, просто оптимизатор gcc более сломан в этом отношении. Предупреждения не следует игнорировать: либо явно отключите оптимизацию -fstrict-aliasing, либо исправьте код. - person Dummy00001; 14.07.2010
comment
@Framester: зависит от того, на что вы хотите портировать. Большинство настольных систем и родственных систем означают одно и то же под 32-битным int, но некоторые из них имеют прямой порядок байтов, а некоторые — прямой, что означает, что порядок байтов в int может варьироваться. - person David Thornley; 14.07.2010
comment
@David: Просто чтобы выбрать нитку: обычный термин - обратный порядок байтов. - person Martin B; 15.07.2010
comment
Спасибо; не знаю, почему я это напечатал. - person David Thornley; 15.07.2010
comment
@Dummy00001 объединение — это официальный способ обеспечить строгое соответствие кода алиасингу. По мнению кого? - person curiousguy; 03.10.2011
comment
@ Dummy00001 - да, сломанный и раздражающий, если у вас есть сотни пакетов разных типов, которые приходят из файлового дескриптора (например, из сети), вам нужно иметь гигантский союз непрозрачного типа, чтобы не отключать строгое сглаживание вообще. - person dashesy; 17.05.2013
comment
Стандарт C11 говорит, что значение не более одного из членов может быть сохранено в объекте объединения в любое время. ($6.7.2.1), так что это будет означать, что использование union таким образом будет неопределенным поведением (это уже было указано мне в другом посте, потому что я сам использовал его). - person kestasx; 24.12.2014
comment
@kestasx см. §6.2.6.1 ¶7: байты..., которые не соответствуют этому члену, но соответствуют другим членам, принимают неуказанные значения, что подразумевает, что байты могут быть переинтерпретированы путем чтения через другой член. Кроме того, это было предметом исправления в ISO C99 TC3 (DR283). - person ninjalj; 13.04.2015
comment
Каков безопасный способ сделать это на С++? - person Ali; 14.06.2016

Проблема возникает из-за того, что вы обращаетесь к массиву символов через double*:

char data[8];
...
return *(double*)data;

Но gcc предполагает, что ваша программа никогда не будет обращаться к переменным через указатели другого типа. Это предположение называется strict-aliasing и позволяет компилятору выполнять некоторые оптимизации:

Если компилятор знает, что ваш *(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;

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

person Lasse Reinhold    schedule 12.10.2012

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

person anon    schedule 22.12.2010
comment
от неписаного члена союза не определено В этом простом случае: union U { int i; short s; } u; u.s=1; return u.i;, да. В общем, зависит. - person curiousguy; 04.10.2011
comment
В C объединение — это четко определенное поведение; в С++ это неопределенное поведение. - person M.M; 24.12.2014


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

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

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

person Jens Gustedt    schedule 14.07.2010
comment
Привет, я заменил игрушечный пример реальной функцией. И int с 3 байтами у меня просто опечатка. - person Framester; 14.07.2010

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

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

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

person supercat    schedule 13.04.2016

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

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

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

or

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

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

char *q = reinterpret_cast<char*>(p);
person Sebastien Mirolo    schedule 16.08.2010
comment
reinterpret_cast это С++. Это С. - person ptomato; 16.08.2010
comment
стандарт позволяет sizeof(char*) отличаться от sizeof(int*) или они могут иметь одинаковый размер, но разное представление, но в любом случае это не имеет ничего общего с проблемой здесь. Этот вопрос касается каламбура, а не представления указателя. char *q = (char*)&p[0] проблема не в том, как заставить два указателя разных типов указывать на один и тот же адрес. Этот вопрос касается каламбура, а не приведения указателя. - person curiousguy; 04.10.2011