为什么将Punning类型视为UB?

问题描述

想象一下:

uint64_t x = *(uint64_t *)((unsigned char[8]){'a','b','c','d','e','f','g','h'});

我有read类型的双关语是未定义的行为。为什么?我实际上是将8个字节的字节重新解释为8个字节的整数。我看不到与union有什么区别,除了类型pun是未定义的行为而union不是。我亲自问了一位程序员,他们说,如果您正在这样做,或者您知道自己在做什么非常好,或者您犯了一个错误。但是社区人士说,应该始终避免这种做法吗?为什么?

解决方法

最终的原因是“因为语言规范是这样说的”。您不必对此争论。如果这就是语言的方式,那就是它的方式。

如果您想了解动机的用法,那就是原始的C语言缺少任何表达两个左值不能互为别名的方式(而现代语言的{{ 1}}关键字仍然对该语言的大多数用户几乎不了解)。无法假设两个左值不能成为别名,意味着编译器无法对内存的每次访问进行重新排序,并且实际上必须为每次对对象的访问执行从内存到内存的负载和存储,而不是将值保留在寄存器中,除非它知道对象的地址从未被占用。

C的基于基于类型的别名规则在某种程度上缓解了这种情况,方法是让编译器假定具有不同类型的左值没有别名。

还请注意,在您的示例中,不仅存在类型拼写问题,而且还存在未对齐问题。 restrict数组没有固有的对齐方式,因此在该地址访问unsigned char会导致对齐错误(由于其他原因,UB),与任何别名规则无关。

,

类型修剪被认为是UB,因为该标准的作者期望在标准未施加任何要求但可以为标准服务的情况下,用于各种目的的质量实现将“以环境的书面形式表现”。预期目的。因此,避免对实现施加过强的授权比要求它们支持程序员所需的一切更为重要。

要改编该示例并从原理上稍微扩展一下,请考虑以下代码(为简单起见,假定为普通的32位实现):

unsigned x;
unsigned evil(double *p)
{
  if (x) *p = 1.0;
  return x;
}
...
unsigned y;
int main(void)
{
  if (&y == &x + 1)
  {
    unsigned res;
    x=1;
    res = evil((double*)&x);
    printf("You get to find out the first word of 1.0; it's %08X.\n",res);
  }
  else
  {
    printf("You don't get to find out the first word of 1.0; too bad.\n");
  }
  return 0;
} 

在没有“严格别名规则”的情况下,编译器处理evil必须考虑到可能在一个实现中发生test所示的调用,如int所示。 double以这样的方式连续地赋值:x可以适合其占用的空间。基本原理的作者认识到,如果编译器返回if看到的x的值,则在这种情况下结果将是“不正确的”,但即使是大多数类型的punning拥护者会承认这样做的编译器(在这种情况下)通常比重新加载union ublob {uint16_t hh[8]; uint32_t ww[4]; } u; int test1(int i,int j) { if (u.hh[i]) u.ww[j] = 1; return u.hh[i]; } int test2(int i,int j) { if (*(u.hh+i)) *(u.ww+j) = 1; return *(u.hh+i); } int test3(int i,int j) { uint16_t temp; { uint16_t *p1 = u.hh+i; temp = *p1; } if (temp) { uint32_t *p2 = u.ww+j; *p2 = 1; } { uint16_t *p3 = u.hh+i; temp = *p3; } return temp; } static int test4a(uint16_t *p1,uint32_t *p2) { if (*p1) *p2 = 1; return *p1; } int test4(int i,int j) { return test4a(u.hh+i,u.ww+j); } (从而生成效率较低的代码)的编译器有用。

请注意,所编写的规则并未描述实现应支持类型修剪的所有情况。给出类似的东西:

test1

如所写,标准中的任何内容均不暗示其中任何一个都将定义行为,除非它们全部都定义了,但是如果union ublob没有定义行为,则在联合内具有数组的功能将变得毫无用处。在支持相关类型的平台上。如果编译器作者意识到对常见类型修剪构造的支持是实现质量问题,那么他们将认识到,对于没有处理前三个问题的实现,几乎没有任何借口,因为任何不是故意盲目的编译器都会看到以下证据:指针都与通用类型test4a的对象有关,而没有义务处理peg = "0123" p1 = [] p2 = [] for i in range(0,len(peg),2): p1.append(peg[i]) for i in range(1,2): p2.append(peg[I]) 中不存在此类证据的可能性。