什么是严格的别名规则?

在询问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 charunsigned char )。 它总是假定char*别名其他类型。 然而,这不会以另一种方式工作:没有假设你的结构化字符的缓冲区。

  • 初学者要小心

    当两种类型叠加时,这只是一个潜在的雷区。 您还应该了解字节顺序,字对齐以及如何通过正确打包结构来处理对齐问题。


    我发现的最好的解释是Mike Acton,了解严格的别名。 它专注于PS3的开发,但基本上只是GCC。

    从文章:

    “严格别名是由C(或C ++)编译器做出的一个假设,即取消引用不同类型对象的指针将永远不会引用相同的内存位置(即彼此相互混淆)。”

    所以基本上,如果你有一个int*指向一个包含一个int内存,然后你将一个float*指向这个内存并将它用作float那么你会违反规则。 如果你的代码不尊重这个,那么编译器的优化器很可能会破坏你的代码。

    规则的例外是char* ,它允许指向任何类型。


    这是C ++ 03标准第3.10节中的严格别名规则(其他答案提供了很好的解释,但是没有人提供规则本身):

    如果程序试图通过以下类型之一的左值访问对象的存储值,则行为是未定义的:

  • 对象的动态类型,
  • 该对象的动态类型的cv限定版本,
  • 类型是对应于对象的动态类型的有符号或无符号类型,
  • 一种类型,即与对象的动态类型的cv限定版本对应的有符号或无符号类型,
  • 包括其成员中的上述类型之一(包括递归地,子成员或包含联合的成员)的聚合或联合类型,
  • 一种类型,它是对象的动态类型(可能是cv-qualified)的基类类型,
  • 一个charunsigned char类型。
  • C ++ 11C ++ 14措辞(重点更改):

    如果程序试图通过非以下类型之一的glvalue访问对象的存储值,则行为未定义:

  • 对象的动态类型,
  • 该对象的动态类型的cv限定版本,
  • 类型(与4.4中定义的)类型的对象的动态类型,
  • 类型是对应于对象的动态类型的有符号或无符号类型,
  • 一种类型,即与对象的动态类型的cv限定版本对应的有符号或无符号类型,
  • 包含其元素中的上述类型之一或非静态数据成员(包括递归地包含子集或包含联合的元素或非静态数据成员)的聚合或联合类型,
  • 一种类型,它是对象的动态类型(可能是cv-qualified)的基类类型,
  • 一个charunsigned 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?