协慌网

登录 贡献 社区

什么是严格别名规则?

当询问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]);   
    }
}

严格的混叠规则使该设置非法的:解引用该别名的对象是不是一个的指针兼容型或其他类型的被 C 2011 6.5 第 7 段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)];
    };
  • 您可以在编译器中禁用严格别名(在 gcc 中f [no-] strict-aliasing ))

  • 您可以使用char*进行别名而不是系统的单词。规则允许char*的异常(包括signed charunsigned char )。始终假设char*别名为其他类型。然而,这不会以另一种方式起作用:没有假设你的结构别名为 chars 的缓冲区。

初学者要小心

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

脚注

1 C 2011 6.5 7 允许左值访问的类型有:

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 与对象的有效类型对应的有符号或无符号类型的类型,
  • 与有效类型的对象的限定版本对应的有符号或无符号类型的类型,
  • 聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),或者
  • 一个字符类型。

我发现的最佳解释是 Mike Acton, 了解严格别名 。它主要关注 PS3 开发,但这基本上只是 GCC。

来自文章:

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

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

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

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

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

  • 对象的动态类型,
  • 一个 cv 限定版本的动态类型的对象,
  • 与对象的动态类型对应的有符号或无符号类型的类型,
  • 一种类型,是有符号或无符号类型,对应于对象动态类型的 cv 限定版本,
  • 一种聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),
  • 一个类型,它是对象动态类型的(可能是 cv 限定的)基类类型,
  • charunsigned char类型。

C ++ 11C ++ 14措辞(强调变化):

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

  • 对象的动态类型,
  • 一个 cv 限定版本的动态类型的对象,
  • 与对象的动态类型类似的类型(如 4.4 中所定义),
  • 与对象的动态类型对应的有符号或无符号类型的类型,
  • 一种类型,是有符号或无符号类型,对应于对象动态类型的 cv 限定版本,
  • 聚合或联合类型,包括其元素或非静态数据成员中的上述类型之一(递归地,包括子聚合或包含联合的元素或非静态数据成员 ),
  • 一个类型,它是对象动态类型的(可能是 cv 限定的)基类类型,
  • charunsigned char类型。

两个变化很小: glvalue而不是lvalue ,以及聚合 / 联合案例的澄清。

第三个变化提供了更强有力的保证(放宽强混叠规则): 类似类型的新概念现在可以安全别名。


C 语言 (C99; ISO / IEC 9899:1999 6.5 / 7; 完全相同的措辞用于 ISO / IEC 9899:2011§6.5¶7):

对象的存储值只能由具有以下类型之一( 73)或 88)的左值表达式访问:

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 与对象的有效类型对应的有符号或无符号类型的类型,
  • 与有效类型的对象的限定版本对应的有符号或无符号类型的类型,
  • 聚合或联合类型,包括其成员中的上述类型之一(包括递归地,子聚合或包含联合的成员),或者
  • 一个字符类型。

73)或 88)此列表的目的是指定对象可能或可能不具有别名的情况。