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

时间:2010-07-14 12:48:35

标签: 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;
    }

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

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

VS

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

[更新] 我用真实的功能取代了玩具示例。

7 个答案:

答案 0 :(得分:39)

出现问题是因为您通过double*

访问了char数组
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;

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

答案 1 :(得分:26)

看起来好像你真的想要使用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);

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

这将以小端格式从文件中读取一个4字节的无符号字符,无论是什么,系统的字节顺序是什么。它应该适用于任何无符号至少为4个字节的系统。

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

答案 2 :(得分:7)

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

答案 3 :(得分:7)

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

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

return *(short*)data;

有了这个:

short temp;
memcpy(&temp, data, sizeof(temp));
return temp;

答案 4 :(得分:2)

基本上你可以把gcc的消息看成是你正在寻找麻烦的家伙,不要说我没有警告你

将一个三字节字符数组转换为int是我见过的最糟糕的事情之一。通常,您的int至少有4个字节。因此,对于第四个(如果int更宽,则可能更多),您将获得随机数据。然后你将所有这些投射到double

不要这样做。与你正在做的事情相比,gcc警告的别名问题是无辜的。

答案 5 :(得分:0)

C标准的作者希望让编译器编写者在理论上可能的情况下生成有效的代码,但不太可能全局变量可能使用看似无关的指针访问其值。这个想法不是通过在单个表达式中转换和取消引用指针来禁止类型惩罚,而是说给出类似的东西:

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

编译器有权假设写入* d不会影响x。标准的作者想要列出这样的情况,即上面接收来自未知来源的指针的函数必须假设它可能为看似无关的全局变为别名,而不要求这些类型完全匹配。不幸的是,虽然理由强烈建议标准的作者在编译器无法相信事情可能是别名的情况下打算描述最低一致性标准,但该规则未能要求编译器在很明显的情况下识别别名 并且gcc的作者已经决定他们宁愿生成最小的程序,同时符合标准的写得不好的语言,而不是生成代码。实际上是有用的,而不是在显而易见的情况下识别别名(虽然仍然能够假设那些看起来不像别名的东西,但不会),他们宁愿要求程序员使用{{1}因此,要求编译器允许未知来源的指针可能对任何内容进行别名,从而阻碍优化。

答案 6 :(得分:-4)

显然,标准允许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);