Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/c/72.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C 为什么类型双关语被认为是UB?_C_Casting_Undefined Behavior_Type Punning - Fatal编程技术网

C 为什么类型双关语被认为是UB?

C 为什么类型双关语被认为是UB?,c,casting,undefined-behavior,type-punning,C,Casting,Undefined Behavior,Type Punning,想象一下: uint64_t x = *(uint64_t *)((unsigned char[8]){'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}); 我有那种双关语,它们是未定义的行为。为什么?我真的在把8字节的字节重新解释成8字节的整数。我看不出这与union有什么不同,除了类型pun是未定义的行为,而unions不是?我亲自问过一位程序员同事,他们说如果你在做,要么你很清楚自己在做什么,要么你在犯错误。但是社区说这种做法应该永远避免?为什么?最终的原因

想象一下:

uint64_t x = *(uint64_t *)((unsigned char[8]){'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'});
我有那种双关语,它们是未定义的行为。为什么?我真的在把8字节的字节重新解释成8字节的整数。我看不出这与
union
有什么不同,除了类型pun是未定义的行为,而
union
s不是?我亲自问过一位程序员同事,他们说如果你在做,要么你很清楚自己在做什么,要么你在犯错误。但是社区说这种做法应该永远避免?为什么?

最终的原因是“因为语言规范这么说”。你没必要为此争论。如果这就是语言的方式,那就是它的方式

如果你想知道这样做的动机,那就是原始的C语言缺乏任何表达两个左值不能相互别名的方式(而现代语言的
restrict
关键字仍然很少被大多数语言用户理解)。无法假定两个左值不能别名意味着编译器无法重新排序加载和存储,并且必须为每次访问对象执行从/到内存的加载和存储,而不是将值保留在寄存器中,除非它知道从未获取过对象的地址

C基于类型的别名规则在某种程度上缓解了这种情况,它允许编译器假定具有不同类型的左值不会产生别名


还请注意,在您的示例中,不仅存在类型双关,而且存在未对齐。
unsigned char
数组没有固有的对齐方式,因此在该地址访问
uint64_t
将是一个对齐错误(另一个原因是UB)独立于任何别名规则。

类型双关被视为UB,因为标准的作者期望用于各种目的的质量实现在标准没有规定要求的情况下会“以环境特有的文件化方式”运行,但如果这种行为能达到预期目的。因此,与要求实现支持程序员所需的一切相比,更重要的是避免对实现强加过强的授权

为了适应和稍微扩展这个例子的理由,考虑代码(假设简单,一个普通的32位实现):

在没有“严格别名规则”的情况下,编译器处理
evil
时必须考虑到它可能被调用的可能性,如
test
中所示,在可能发生的情况下,将两个
int
值连续放置在
double
所占用的空间中。基本原理的作者认识到,如果编译器返回了
if
所看到的
x
值,那么在这种情况下,结果将是“不正确的”,但即使是大多数类型双关的拥护者也会承认这样做的编译器(在类似的情况下)通常比重新加载
x
(从而生成效率较低的代码)的代码更有用

注意,所编写的规则并不是描述实现应该支持类型双关的所有情况。假设:

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
在支持所讨论类型的平台上没有定义的行为,那么在联合中拥有数组的能力将毫无用处。如果编译器编写者认识到支持通用类型双关结构是实现质量的问题,那么他们就会认识到实现没有理由不处理前三个问题,因为任何不是故意盲的编译器都会看到指针都与公共类型的对象相关的证据,而不会觉得有义务在
test4a
中处理这样的可能性,因为那里不存在这样的证据。

什么对齐方式?那么
限制
又做了什么呢?这是否意味着告诉编译器标有
restrict
的指针是访问特定内存的唯一方法?Re:alignment,类型为
T
的声明对象的地址具有alignment
\u Alignof(T)
。通过
malloc
获得的地址具有适合于任何标准类型存储的对齐方式。如果
p
为类型
T
对齐,
(无符号字符*)p+n
为类型
T
对齐,当且仅当定义了和且
n
的倍数时
。非常粗略地
对指针进行限制
会强加一个契约,即除非在指针的生命周期内通过该指针,否则您将无法访问指向的对象。精确的规格要复杂得多,这就是为什么没有人理解它-那么,如果对齐是
\u Alignof(T)
的值,那么
\u Alignof(T)
是什么呢?它是否等同于
&T%sizeof(T)
。不是我的DV,而是C11和更高版本在6.5.2.3结构和联合成员中有一个脚注:如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,值的对象表示的适当部分被重新解释为新类型中的对象表示,如6.2.6所述(这一过程有时被称为“类型双关”)。这可能是一个陷阱表示。这应该使
test1
test2
得到定义,而不是
test3
test4
@chqrlie:我在标准中没有看到任何东西表明直接使用数组衰减形成的指针与存储它然后使用它有不同的语义。我认为,合理的说法是,当“新的和可见的派生”时,必须使用这些指针,这将是#1、#2和#3中的情况,而不是#4中的情况,但什么都没有
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);
}