【问题标题】:Unions and strict aliasing in C11C11 中的联合和严格别名
【发布时间】:2016-11-21 23:24:29
【问题描述】:

假设我有这样的工会

union buffer {
  struct { T* data; int count; int capacity; };
  struct { void* data; int count; int  capacity; } __type_erased;
};

如果我在 C11 别名规则下混合读取/写入匿名结构成员和 __type_erased 成员,我会遇到麻烦吗?

更具体地说,我对独立访问组件(例如通过不同的指针)时发生的行为感兴趣。举例说明:

grow_buffer(&buffer.__type_erased);
buffer.data[buffer.count] = ...

我已经阅读了我能找到的所有相关问题,但我仍然不是 100% 清楚这一点,因为有些人似乎认为这种行为是未定义的,而另一些人则认为这是合法的。此外,我发现的信息是 C++、C99、C11 等规则的混合体,很难消化。在这里,我对 C11 强制执行并由流行编译器(Clang、GCC)展示的行为非常感兴趣

编辑:更多信息

我现在已经对多个编译器进行了一些实验,并决定分享我的发现,以防有人遇到类似问题。我的问题的背景是我试图用纯 C 编写一个用户友好的高性能通用动态数组实现。这个想法是数组操作是使用宏和重型操作(如增长数组)进行的使用别名类型擦除的模板结构执行。例如,我可以有这样的宏:

#define ALLOC_ONE(A)\
    (_array_ensure_size(&A.__type_erased, A.count+1), A.count++)

必要时增加数组并返回新分配项的索引。规范(6.5.2.3)规定允许通过不同的联合成员访问同一位置。我对此的解释是,虽然 _array_ensure_size() 不知道联合类型,但编译器应该知道成员 __type_erased 可能会因副作用而发生变异。也就是说,我认为这应该可行。然而,这似乎是一个灰色地带(老实说,规范并不清楚什么构成了成员访问)。苹果最新的 Clang (clang-800.0.33.1) 没有问题。代码在没有警告的情况下编译并按预期运行。但是,当使用 GCC 5.3.0 编译时,代码会因段错误而崩溃。事实上,我强烈怀疑 GCC 的行为是一个错误——我尝试通过删除可变指针 ref 并采用清晰的函数式样式来明确联合成员突变,例如:

#define ALLOC_ONE(A) \
   (A.__type_erased = _array_ensure_size(A.__type_erased, A.count+1),\
    A.count++)

正如预期的那样,这再次与 Clang 一起工作,但再次使 GCC 崩溃。我的结论是,使用联合的高级类型操作是一个应该谨慎行事的灰色地带。

【问题讨论】:

  • 对于非常好奇的人,我已经设法通过仅使用宏和完全使用联合来解决问题。如果容量超过阈值,我只使用一个复杂的宏,它使用逗号表达式重新分配()数组数据,而不是使用函数来增长数组。这适用于 GCC 和 Clang,而且速度也非常快。

标签: language-lawyer unions c11 strict-aliasing


【解决方案1】:

C11 标准规定如下:

6.5.2.3 结构和联合成员

95) 如果用于读取联合对象内容的成员不是 与上次用于在对象中存储值的成员相同, 值的对象表示的适当部分是 如所述,重新解释为新类型中的对象表示 在 6.2.6 中(有时称为“类型双关语”的过程)。这可能是 陷阱表示。

因此,从 C11 中联合字段读/写的角度来看,这是正确的。但是严格别名是基于类型的分析,所以它的幼稚实现可以说这些读/写操作是独立的。据我了解,现代 gcc 可以检测带有联合字段的情况并避免此类错误。

另外你应该记住,在某些情况下,指向联合成员的指针是无效的:

以下不是有效片段(因为联合类型不是 在函数 f) 中可见:

struct t1 { int m; };
struct t2 { int m; };
int f(struct t1 *p1, struct t2 *p2)
{
  if (p1->m < 0)
  p2->m = -p2->m;
  return p1->m;
}
int g()
{
  union {
    struct t1 s1;
    struct t2 s2;
  } u;
  /* ... */
  return f(&u.s1, &u.s2);
}

在我看来,使用工会在不同的成员中读/写是危险的,最好避免它。

【讨论】:

  • 联合类型在f 中是否可见并不重要,重要的是访问是否通过联合类型的左值完成。即在文件中更早地移动联合的定义不会改变任何东西。
  • @JonathanWakely 是的,你是对的。但这是来自标准的引用。从编译器的角度来看,在 f 中制作 sa 时,您真的看不到调用点中的参数是一个联合的成员
  • 哦,对了,对不起,我没有意识到这部分是直接从标准中引用的。你是对的,因为它看不到类型,它肯定不能通过该类型进行访问!
  • 有趣!我可以清楚地看到 g() 如何违反混叠条件,但我的情况有点不同。确实,我正在使用指向联合成员的指针并将其传递给看不到联合类型的函数,但是,我不会将别名指针发送到同一个函数,编译器应该知道联合可以是通过该指针更改。我已经编辑了我的问题,以添加一些尝试 Clang 和 GCC 5 的结果,这些结果确实在这里显示出非常不同的行为。
  • @JonathanWakely:gcc 的作者可能不喜欢它,但是如果编译器遵守标准,关于 complete 联合类型可见的语言会起到有用的作用,而它被 gcc 的解释变得毫无意义。如果编译器遵循书面标准,但包含一个#pragma,它告诉编译器它不需要尊重特定联合成员的通用初始序列规则,那么书面规则不会使任何优化成为不可能。如果编译器忽略了要求它在没有相反指令的情况下识别别名的规则......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-29
  • 2012-02-14
相关资源
最近更新 更多