作为参考,我试图理解本文中的示例:

总结如下:

int foo(int *a, long *b)
{
    int t = *a;
    *b = 0;          // cannot change *a
    return *a - t;   // can be folded to zero
}

int bar(int *a, long *b)
{
    int t = *a;
    for (int i = 0; i != sizeof *b; ++i)
    ((unsigned char*)b)[i] = 0;
    
    return *a - t;   // must not be folded
}

文章声称foo的回报是可以折叠的:

由于ab被声明为指向不兼容类型的指针,并且由于 C 和 C++ 要求对象的存储值只能由兼容类型的左值访问,因此存储到不能影响缓存在变量中*b的值(通常是寄存器)。因此,减法表达式中的操作数必须相等,并且结果必须为零。*at

但我不明白为什么在 中会有所不同bar。哪些代码/事件序列会导致折叠无效?

12

  • 这肯定是文章本身的问题。在 的例子中*b = 0;a永远不会通过 引用b,也b永远不会通过 引用a,因此根本不存在别名问题。如果意图是您不应该通过 引用ab反之亦然,那只是严格别名规则本身的陈述。longint不是兼容类型。 的值*a不会神奇地缓存在 中tt它只是函数的一个局部变量,存储在 持有的地址中的值被赋值a给该函数。这段话不清楚,不要过多解读。


    – 

  • 这个问题类似于:。如果您认为它们不同,请问题,说明它们有何不同和/或该问题的答案对您的问题无济于事。


    – 

  • 2
    C++ 有三种特殊类型允许违反严格别名规则: char*unsigned char*std::byte*。en.cppreference.com /…(这是 C++;C 规则不同)


    – 


  • 请注意,链接的问题用 C 语言解释了这一点。C++ 的规则略严格一些;只允许有符号/无符号变体、类型本身,以及 bye + char + unsigned char。不是有符号字符或联合


    – 


  • 有争议的规则是该规则已在本网站上讨论过多次。C++ 等效规则位于


    – 


最佳答案
2

unsigned char类型别名规则对于通过类型(以及char和)的 glvalue 访问任何类型的对象有一个特定的例外std::byte。对于这些类型,并且只有这些类型,在别名规则下不存在编译器可以利用进行优化的未定义行为。C 具有与别名规则类似的例外,但在对象模型方面有不同规定。

如果ab表示相同的地址,则*b = 0int t = *a;必定具有未定义的行为,因为在该地址处只能存在int或一个long对象,并且只能访问该对象。另一种访问要么尝试访问超出生存期的对象,要么尝试通过不同类型的泛左值访问一种类型的对象,从而违反类型别名规则。其中一个必须导致未定义的行为,因此编译器可以假设这种情况(即a和表示相同的地址)不会发生。(指向和对象的b内存范围也不能重叠,因为对象和对象不能同时存在于重叠存储中。)intlongintlong

在第二个示例中,*b = 0被替换为((unsigned char*)b)[i] = 0。 指针b永远不会作为long类型访问,而只能作为 访问unsigned char。 由于上述例外,如果ab都指向同一类型的对象,int即它们都表示的地址处 (唯一) 存活的对象,则根据别名规则,这不是 UB。 因此,编译器不能假设ab表示不同的地址。

话虽如此,C++ 标准缺乏关于通过 glvalue 进行访问unsigned char或指针上的指针算法unsigned char*应该如何表现的任何实际定义,因此,从严谨的角度阅读该标准无论如何它仍然是 UB。

但是,通常的假设是,它的行为应该好像unsigned char*转换的指针结果是指向原始对象的对象表示的指针,被视为数组unsigned char。这在很多边缘情况下仍然留下了很多不清楚的地方,但解释了常见用法通常如何按预期工作。

基于这种理解,将直接修改上述有效场景中指向的对象((unsigned char*)b)[i] = 0的对象表示。更改对象的对象表示意味着更改其值,因此编译器不能假设后面的读取将产生与写入之前相同的值,而必须重新加载它。intaint*a

  • 在 中foo,赋值*b = 0不会产生影响,*a因为在严格的别名规则下类型不兼容。这使得编译器可以安全地折叠*a - t为零。
  • 然而,在 bar 中,将 b 强制转换为unsigned char*并逐字节写入意味着编译器必须考虑到*a在循环中被修改的可能性。因此,*a - t不能假设 的值是零,并且编译器必须避免折叠此表达式。

关键区别在于是unsigned char*一种绕过严格别名规则的特殊类型,允许通过间接修改int指向的对象ab