C/C++中的严格别名规则是怎么回事

我相信很多人都写过下面的代码,然而有的大神会说这个代码有问题,这是为什么呢? 

#include <stdio.h>
int main()
{
        float f = 1.2f;
        int i = *(int *)pf;
        printf("%x\n", i);
}

原来,C/C++标准中有一条叫做严格别名规则。在严格别名规则约束下,任何类型的指针(不包括C++中的成员指针),都不能与类型无关的指针重叠。一个例外就是char*、signed char*和unsigned char*是可以与任何指针重叠的。

严格别名规则只有在GCC和Clang使用-O2、-O3、-Os优化级别的情况下才会出问题,因为这些优化级别包含-fstrict-aliasing。

上面的代码使用g++ -O2 -Wall编译,会一个警告,但是执行并不会有问题,因为代码太简单了,没有可以优化的地方。实际上真正出问题的是下面这个代码。

下面这个代码使用g++ -O2 -Wall编译,不会有警告,但其中的if会被优化掉,因为编译器假设h与k不会指向同一块内存。所以根据警告判断是否会出问题是靠不住的。

#include <stdio.h>
void check(short *h, long *k)
{
        *h = 5;
        *k = 6; // 编译器认为不会改变*h
        if (*h == 5)
                printf("strict aliasing problem\n");
}
int main()
{
        long k[1];
        check((short *)k, k);
}

还有一个问题是,严格别名规则是不能传递的,也就是说,你不能通过char*作为中介,把int*和float*联系为指向同一个变量,编译器只会注意到指针解引用操作,不会注意到你的转换过程。比如下面这个代码仍然会出问题:

#include <stdio.h>
void check(short *h, long *k, char*c)
{
        *h = 5;
        *k = 6;
        *(int*)c = 7; // 编译器仍然认为不会改变*h
        if (*h == 5)
                printf("strict aliasing problem\n");
}
int main()
{
        long k[1];
        check((short *)k, k, (char*)k);
}

但是下面的这一个代码就不会出问题,因为直接解引用了char*:

#include <stdio.h>
void check(short *h, long *k, char*c)
{
        *h = 5;
        *k = 6;
        *c = 7; // 编译器认为*h、*k均可能改变
        if (*h == 5)
                printf("strict aliasing problem\n");
}
int main()
{
        long k[1];
        check((short *)k, k, (char*)k);
}

其实这种优化已经很多人抱怨过了,因为这相当于「给了工具,却不让用」。在C/C++的优势领域系统编程中,指针转换几乎是最快的数据重解析方法。特别是在C++中,reinterpret_cast的名字写得很具有吸引力,标准却不让用,确实让人纠结。这几乎相当于「自废武功」。

对于这个问题,有很多种解决方法:

1、可以使用-fno-strict-aliasing关闭与严格别名规则相关的优化,或使用-O0、-O1、-Og这些不打开-fstrict-aliasing的优化级别。实际上Linux内核等著名软件也关闭了这种优化。还可以看一下这几篇文章介绍这一著名的口水战。

https://zine.la/article/686f5e1e554a4f439bef689005771519/

https://www.oschina.net/news/96906/linus-was-angry-about-standard-saying?p=1

这篇文章还提到了一个编译选项-fwrapv,这个是将有符号数溢出定义为2的补码回卷,也就是说使用这个选项,有符号数溢出将不再是未定义行为。这个编译选项Linux内核并没有使用,不过很多其它的项目使用了这个编译选项。

有符号数溢出是未定义行为,可以体现为下面的代码经优化后永远返回false:

bool testoverflow(int a)
{
        if (a + 1 < a)
                return true;
        return false;
}

严格别名规则和有符号数溢出限制了很多常见的系统编程技巧。所以-fno-strict-aliasing -fwrapv这些编译开关在系统编程中非常有用也非常常用。实际上,我们不必为了迎合C/C++标准对于未定义行为的规定而束缚手脚,如果确实有必要使用指针转换和有符号数溢出,以编写效率更高的代码,只需加上对应的编译器开关即可。

另外,在Visual C++中并不存在这种编译器唯标准的做法,Visual C++会按照习惯用法进行实现,一个例子就是Visual C++甚至可以混用delete和delete[],所以Visual C++不需要加这些开关。

这篇文章解释了Clang是如何看待和处理未定义行为的:

https://blog.csdn.net/monkey07118124/article/details/50588336

https://www.cnblogs.com/foohack/p/3582239.html

2、可以使用memcpy(推荐)或字节拷贝进行数据重解析。这种方法并不违反严格别名规则,因此无论使用什么优化级别,都不会出问题。

// 使用memcpy(推荐)
#include <stdio.h>
#include <string.h>
int main()
{
        float f = 1.2f;
        int i;
        memcpy(&i, &f, sizeof i);
        printf("%x\n", i);
}
// 使用字节拷贝
#include <stdio.h>
int main()
{
        float f = 1.2f;
        int i;
        char *pf = (char *)&f;
        char *pi = (char *)*i;
        for (int n = 0; n < sizeof i; n++)
                pi[n] = pf[n];
        printf("%x\n", i);
}

3、可以使用union进行数据重解析。虽然从标准上来说仅适用于C,不适用于C++,但是大多数编译器即使在C++下也接受这种写法。

#include <stdio.h>
union float_or_int
{
        float f;
        int i;
};
int main()
{
        union float_or_int fi;
        fi.f = 1.2f;
        printf("%x\n", fi.i);
}
发布了29 篇原创文章 · 获赞 1 · 访问量 3395

猜你喜欢

转载自blog.csdn.net/defrag257/article/details/103929865