什么是严格的别名规则?
在询问C语言中常见的未定义行为时,灵魂比我提到严格的别名规则更加开明。
他们在说什么?
遇到严格别名问题的典型情况是将结构(如设备/网络信息)覆盖到系统字长度的缓冲区上(如指向uint32_t
s或uint16_t
s的指针)。 当通过指针转换将结构覆盖到这样的缓冲区或缓冲区上时,您可以轻松违反严格的别名规则。
所以在这种设置中,如果我想发送消息给某个东西,我必须有两个不兼容的指针指向同一块内存。 然后我可能天真地编码这样的东西:
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
严格的别名规则使得这种设置非法:取消引用另一个不兼容类型的别名是未定义的行为。 不幸的是,你仍然可以用这种方式进行编码,也许会得到一些警告,编译好,只是在运行代码时出现奇怪的意外行为。
(海湾合作委员会在提供别名警告的能力方面似乎有些不一致,有时会给我们一个友好的警告,有时候不会。)
为了明白为什么这个行为是未定义的,我们必须考虑严格的别名规则是如何购买编译器的。 基本上,通过这条规则,不必考虑插入指令来刷新循环中每次运行的buff
的内容。 相反,在进行优化时,如果有一些恼人的未被强制设置的混叠假设,可以省略这些指令,在循环运行之前将buff[0]
和buff[1
]加载到CPU寄存器中,并加速循环体。 在引入严格的别名之前,编译器必须处于一种偏执狂的状态,即任何人随时随地都可以改变buff
的内容。 因此,为了获得额外的性能优势,并假设大多数人不会输入双关键字,就会引入严格的别名规则。
请记住,如果你认为这个例子是人为设计的,那么如果你将一个缓冲区传递给另一个为你发送数据的函数,甚至可能会发生这种情况。
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
并重写我们早期的循环,以利用这个便利的功能
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
编译器可能会或可能无法或足够聪明地尝试内联SendMessage,它可能会或可能不会决定加载或不加载buff。 如果SendMessage
是单独编译的另一个API的一部分,它可能有加载buff内容的指令。 然后再次,也许你在C ++中,这是编译器认为它可以内联的一些模板头只有实现。 或者,也许这只是为了您的方便,您在.c文件中编写的内容。 无论如何,未定义的行为可能仍然会发生。 即使我们知道一些发生了什么,但仍然违反了规则,因此没有明确定义的行为得到保证。 所以只需通过包装我们的单词分隔缓冲区的功能就不一定有帮助。
那么我该如何解决这个问题?
使用联合。 大多数编译器都支持这一点,而不会抱怨严格的别名。 这在C99中是允许的,并且在C11中明确允许。
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
您可以在编译器中禁用严格别名(f [no-] gcc中的严格别名))
您可以使用char*
作为别名,而不是系统的单词。 规则允许char*
的异常(包括signed char
和unsigned char
)。 它总是假定char*
别名其他类型。 然而,这不会以另一种方式工作:没有假设你的结构化字符的缓冲区。
初学者要小心
当两种类型叠加时,这只是一个潜在的雷区。 您还应该了解字节顺序,字对齐以及如何通过正确打包结构来处理对齐问题。
我发现的最好的解释是Mike Acton,了解严格的别名。 它专注于PS3的开发,但基本上只是GCC。
从文章:
“严格别名是由C(或C ++)编译器做出的一个假设,即取消引用不同类型对象的指针将永远不会引用相同的内存位置(即彼此相互混淆)。”
所以基本上,如果你有一个int*
指向一个包含一个int
内存,然后你将一个float*
指向这个内存并将它用作float
那么你会违反规则。 如果你的代码不尊重这个,那么编译器的优化器很可能会破坏你的代码。
规则的例外是char*
,它允许指向任何类型。
这是C ++ 03标准第3.10节中的严格别名规则(其他答案提供了很好的解释,但是没有人提供规则本身):
如果程序试图通过以下类型之一的左值访问对象的存储值,则行为是未定义的:
char
或unsigned char
类型。 C ++ 11和C ++ 14措辞(重点更改):
如果程序试图通过非以下类型之一的glvalue访问对象的存储值,则行为未定义:
char
或unsigned char
类型。 两个变化很小:流值而不是左值,澄清汇总/工会案例。
第三个变化提供了更强的保证(放宽强烈的别名规则):现在可以安全别名的类似类型的新概念。
C语言 (C99; ISO / IEC 9899:1999 6.5 / 7;在ISO / IEC 9899:2011§6.5¶7中使用完全相同的措辞):
对象的存储值只能由具有以下类型之一的左值表达式访问:73)或88):
73)或88)这份清单的目的是规定对象可以或不可以被别名的情况。
链接地址: http://www.djcxy.com/p/36289.html上一篇: What is the strict aliasing rule?
下一篇: Is it better to use std::memcpy() or std::copy() in terms to performance?