取消引用类型惩罚指针将破坏严格别名规则

Fra*_*ter 45 c optimization gcc pointers strict-aliasing

我使用以下代码来从文件中读取数据,作为更大程序的一部分.

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;
    }
Run Code Online (Sandbox Code Playgroud)

现在我被告知使用-O2,我得到以下gcc警告: warning: dereferencing type-punned pointer will break strict-aliasing rules

谷歌我找到了两个正交的答案:

VS

最后我不想忽视这些警告.你会推荐什么?

[更新]我用真实的功能代替了玩具例子.

小智 39

出现此问题的原因是您通过以下方式访问char数组double*:

char data[8];
...
return *(double*)data;
Run Code Online (Sandbox Code Playgroud)

但gcc假设您的程序永远不会通过不同类型的指针访问变量.这个假设称为严格别名,允许编译器进行一些优化:

如果编译器知道你的*(double*)can不会与之重叠data[],那么就允许各种各样的事情,例如将代码重新排序为:

return *(double*)data;
for(int i=7;i>=0;i--)
    data[i] = fgetc(stream);
Run Code Online (Sandbox Code Playgroud)

循环最有可能被优化掉,你最终得到:

return *(double*)data;
Run Code Online (Sandbox Code Playgroud)

这使您的数据[]未初始化.在这种特殊情况下,编译器可能会看到您的指针重叠,但如果您已经声明它char* data,它可能会给出错误.

但是,严格别名规则说char*和void*可以指向任何类型.所以你可以把它重写成:

double data;
...
*(((char*)&data) + i) = fgetc(stream);
...
return data;
Run Code Online (Sandbox Code Playgroud)

严格的别名警告对于理解或修复非常重要.它们会导致内部无法重现的错误类型,因为它们只出现在一台特定计算机上某个特定操作系统上的一个特定编译器上,而且只发生在满月和一年一次等等.


Mar*_*n B 26

它看起来很像你真的想要使用fread:

int data;
fread(&data, sizeof(data), 1, stream);
Run Code Online (Sandbox Code Playgroud)

也就是说,如果您确实想要读取字符的路径,然后将它们重新解释为int,那么在C中(但不是在C++中)这样做的安全方法是使用union:

union
{
    char theChars[4];
    int theInt;
} myunion;

for(int i=0; i<4; i++)
    myunion.theChars[i] = fgetc(stream);
return myunion.theInt;
Run Code Online (Sandbox Code Playgroud)

我不确定为什么data原始代码的长度是3.我假设你想要4个字节; 至少我不知道int是3个字节的任何系统.

请注意,您的代码和我的代码都非常不便携.

编辑:如果你想从文件中读取各种长度的整数,便携式,尝试这样的事情:

unsigned result=0;
for(int i=0; i<4; i++)
    result = (result << 8) | fgetc(stream);
Run Code Online (Sandbox Code Playgroud)

(注意:在实际程序中,您还需要针对EOF测试fgetc()的返回值.)

无论系统的字节顺序如何,它都会以小端格式从文件中读取4字节的无符号字符.它应该适用于无符号至少为4个字节的任何系统.

如果你想要端点中立,不要使用指针或联合; 改为使用位移.

  • +1.再次强调:联合是保持代码严格别名兼容的官方方式.这不是gcc特有的,它只是gcc的优化器在这方面更加突破.不应忽略警告:显式禁用-fstrict-aliasing优化或修复代码. (7认同)
  • @ Dummy00001"_an union是一种保持代码严格别名的官方方式."根据谁的说法? (4认同)

小智 7

在这里使用联合不是正确的做法.从一个未写入的联合成员读取是未定义的 - 即编译器可以自由地执行将破坏您的代码的优化(如优化写入).

  • 在C中,联合是明确定义的行为; 在C++中,它是未定义的行为. (4认同)

小智 7

本文总结了这种情况:http://dbp-consulting.com/tutorials/StrictAliasing.html

那里有几种不同的解决方案,但最便携/安全的解决方案是使用memcpy().(函数调用可能会被优化掉,所以它不像它看起来那么低效.)例如,替换它:

return *(short*)data;
Run Code Online (Sandbox Code Playgroud)

有了这个:

short temp;
memcpy(&temp, data, sizeof(temp));
return temp;
Run Code Online (Sandbox Code Playgroud)