【问题标题】:Can a type which is a union member alias that union?作为联合成员的类型可以为该联合别名吗?
【发布时间】:2019-06-28 08:16:54
【问题描述】:

this question提示:

C11 standard 声明指向联合的指针可以转换为指向其每个成员的指针。来自第 6.7.2.1p17 节:

联合的大小足以容纳最大的 其成员。最多一个成员的值可以是 随时存储在联合对象中。 指向联合的指针 适当转换的对象指向其每个成员(或 如果成员是位域,则指向它所在的单元 驻留),反之亦然。

这意味着您可以执行以下操作:

union u {
    int a;
    double b;
};

union u myunion;
int *i = (int *)&u;
double *d = (double *)&u;

u.a = 2;
printf("*i=%d\n", *i);
u.b = 3.5;
printf("*d=%f\n", *d);

但是反过来呢:在上述联合的情况下,int *double * 可以安全地转换为union u * 吗?考虑以下代码:

#include <stdio.h>

union u {
    int a;
    double b;
};

void f(int isint, union u *p)
{
    if (isint) {
        printf("int value=%d\n", p->a);
    } else {
        printf("double value=%f\n", p->b);
    }
}

int main()
{
    int a = 3;
    double b = 8.25;
    f(1, (union u *)&a);
    f(0, (union u *)&b);
    return 0;
}

在本例中,指向intdouble 的指针,它们都是union u 的成员,被传递给一个需要union u * 的函数。一个标志被传递给函数来告诉它访问哪个“成员”。

假设,如本例,访问的成员与实际传入的对象的类型相匹配,上述代码是否合法?

我在 gcc 6.3.0 上使用 -O0-O3 编译了这个并且都给出了预期的输出:

int value=3
double value=8.250000

【问题讨论】:

  • 您甚至不需要别名规则来查看这可能具有标准未定义的行为。如果double 需要八字节对齐,那么联合也需要。但是int a 可能只有四字节对齐,在这种情况下将&amp;a 转换为union u * 的行为没有定义。

标签: c language-lawyer unions strict-aliasing


【解决方案1】:

在这个例子中,指向 int 和 double 的指针,它们都是成员 union u 中的一个,被传递给一个函数,在该函数中需要一个 union u *。一种 标志被传递给函数以告诉它要访问哪个“成员”。

假设在这种情况下,访问的成员与类型匹配 实际传入的对象,上面的代码合法吗?

您似乎将分析的重点放在工会成员类型上的严格别名规则上。但是,鉴于

union a_union {
    int member;
    // ...
} my_union, *my_union_pointer;

,我倾向于认为my_union.membermy_union_pointer-&gt;member 形式的表达式除了访问成员类型的对象外,还表示访问union a_union 类型的对象的存储值。因此,如果my_union_pointer 实际上没有指向有效类型为union a_union 的对象,那么确实违反了严格的别名规则——关于类型union a_union——因此行为是未定义的。

【讨论】:

  • 我不确定这是否适用,至少不适用于-&gt;。 6.5.3.2p4:后缀表达式后跟-&gt; 运算符和标识符指定结构或联合对象的成员。该值是第一个表达式指向的对象的命名成员的值,并且是一个左值。如果第一个表达式是指向限定类型​​的指针,则结果具有指定成员类型的限定版本。
  • 好吧,@dbush,我认为该文本是为了支持我的立场,因为它是根据操作数表达式指向的对象的成员编写的 .但是如果你看不清楚你的方式,那么它仍然会给你留下 UB,因为如果操作数实际上并不指向联合,那么就没有该联合的成员可以从中提取值,也没有定义评估表达式的任何其他方式。
  • 没有“同时指向多个有效类型的指针”的概念。当然,标准的一部分写得不好,但它没有提到你的任何猜测。如果是这样,他们就不会对聚合和联合类型进行例外处理。值得注意的是,这些规则不关心指针指向的位置,只关心通过哪种类型访问数据。
  • @Lundin,我知道你知道,指针只有一种目标类型。但是具有不同有效类型的多个对象当然可以在内存中重叠,并且标准当然认识到转换指针的类型可以改变结果指向的那些。但是该标准没有提供行为u-&gt;b 的任何定义,如果u 实际上没有指向其有效类型是u 的目标类型的对象,无论该对象的有效类型与u 被转换为其他指针类型的结果。
  • 有效类型是联合成员的类型,具体取决于值访问的完成方式。如果你做了类似*my_union = *my_other_union; 的事情,那么有效的类型就是联合类型。但是如果你做了my_union-&gt;foo = 5;,那么有效类型是int等等。但是你仍然可以访问有效类型int作为联合驻留的整个内存区域,因为它是一个联合类型,包含与有效类型(int)。
【解决方案2】:

标准没有给予使用成员类型的左值访问structunion 对象的一般权限,也没有——据我所知——它没有给予任何特定的权限来执行这种访问,除非成员恰好是字符类型。它也没有定义任何将int* 转换为union u* 的行为可以创建一个不存在的方法。相反,创建将作为union u 访问的任何存储意味着同时在该存储中创建union u 对象。

相反,该标准(引用自 C11 草案 N1570 的引用)依赖于应用脚注 88 的实现(此列表的目的是指定对象可能或可能不会被别名的情况。) 并认识到“严格别名规则”(6.5p7) 仅应在通过其自身类型的左值和另一类型的看似无关的左值引用对象时应用在函数或循环的某些特定执行期间[即当对象为其他一些左值起别名时]。

两个左值何时可能被视为“看似不相关”,以及何时应期望实现识别它们之间的关系的问题是实现质量问题。 Clang 和 gcc 似乎认识到具有 unionPtr-&gt;valueunionPtr-&gt;value[index] 形式的左值与 *unionPtr 相关,但似乎无法认识到指向此类左值的指针与 unionPtr 有任何关系。因此他们将认识到unionPtr-&gt;array1[i]unionPtr-&gt;array2[j] 都可以访问*unionPtr(因为通过[] 的数组下标似乎与数组到指针衰减的处理方式不同),但不会认识到*(unionPtr-&gt;array1+i) 和@987654337 @也一样。

附录--标准参考:

给定

union foo {int x;} foo,bar;
void test(void)
{
  foo=bar;   // 1
  foo.x = 2; // 2
  bar=foo;   // 3
}

标准将foo.x 的类型描述为int。如果第二条语句没有访问foo 的存储值,那么第三条语句将无效。因此,第二条语句使用int 类型的左值访问union foo 类型对象的存储值。看着 N1570 6.5p7:

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

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

脚注 88)此列表的目的是指定对象可能会或可能不会被别名的情况。

请注意,上面没有授予使用int 类型的左值访问union foo 类型的对象的权限。因为上面是一个约束,所以任何违反它都会调用 UB 即使该构造的行为将由标准定义

【讨论】:

  • “标准没有给予使用成员类型的左值访问结构或联合对象的一般权限”当然可以,问题的第一部分引用了相关部分。这意味着如果您通过强制转换从联合指针转到成员类型指针,您最终会得到例如int* 指向int。这当然很好,严格的别名甚至不适用。
  • @Lundin:根据 6.5p7,union foo 类型的对象的存储值只能由 union foo 类型的左值访问,其他结构或联合具有 union foo 作为成员,或字符类型,以及任何违反该约束的程序都会调用 UB,即使在没有该约束的情况下,其行为将在标准中的其他地方定义
  • 根据这个逻辑,这就是UB:int* iptr = &amp;my_union.my_int; *iptr = 0;。这会破坏整个语言。必须始终将聚合/联合的每个成员视为与其声明的类型具有相同的有效类型,否则没有任何意义。
  • @Lundin:clang/gcc 的作者认为构造是 UB,两者都不会一致地处理它。我认为他们认为 UB 是正确的,但这只是因为标准的作者没有努力禁止编译器以愚蠢无用的方式行事——他们在基本原理中明确承认了这一事实。 6.5p7 的目的是指示事物可能别名的情况,而不是从其他类型的对象明显新派生的指针/左值可用于访问这些对象的情况。
  • @Lundin:有关 clang/gcc 无法使用联合成员有意义地处理 address-of 的示例,请参阅godbolt.org/z/Lw5zOPtest 的生成代码忽略了对 @987654353 的操作的可能性@ 可能会影响arr[i],而test2 会忽略i==0 j==0 的可能性,其中(&amp;arr[0].s1+i)-&gt;m 的行为应该等同于arr[0].s1.m,但不会。
【解决方案3】:

关于严格别名,从指向类型的指针(例如&amp;a)到包含该类型的指向联合的指针没有问题。它是严格别名规则的例外之一,C17 6.5/7:

对象的存储值只能由具有以下类型之一的左值表达式访问:
- 与对象的有效类型兼容的类型,/--/
- 聚合或联合类型,其中包括上述类型之一 成员

因此,只要union 包含int/double,就可以使用严格的别名。并且指针转换本身也是明确定义的。

当您尝试访问内容时会出现问题,例如将int 的内容作为更大的double。出于多种原因,这可能是 UB - 我至少可以想到 C17 6.3.2.3/7:

指向对象类型的指针可以转换为指向不同对象类型的指针。如果结果指针未正确对齐69),则行为未定义。

非规范脚注提供更多信息的地方:

69) 一般来说,“正确对齐”的概念是可传递的:如果指向类型 A 的指针与指向类型 B 的指针正确对齐, 反过来,它对于指向 C 类型的指针正确对齐,然后指向 A 类型的指针对于指向 C 类型的指针正确对齐。

【讨论】:

  • 我认为对齐是这里的关键。我没有考虑过这个角度,根据你引用的段落,这最终被拒绝,暴露了可能会导致对齐的丑陋黑客。
  • @dbush 或者更根本的是:对象大小。但这隐含地被对齐覆盖了。
【解决方案4】:

没有。这在形式上并不正确。

在 C 中你可以做任何事情,它也可以工作,但是像这样的结构就是炸弹。未来的任何修改都可能导致重大失败。

联合保留内存空间来保存 最大的 it 元素:

联合的大小足以容纳其最大的 成员。

反面空间不够。

考虑:

union
{
    char a;
    int b;
    double c;
} myunion;
char c;
((union myunion *)&c)->b = 0;

会造成内存损坏。

标准定义的含义:

最多可以将一个成员的值存储在一个联合中 随时反对。一个指向联合对象的指针,经过适当转换, 指向它的每个成员(或者如果一个成员是一个位域,那么 它所在的单元),反之亦然。

强制每个联合成员从联合起始地址开始,并且隐式声明编译器应在其每个元素的合适边界上对齐联合,这意味着选择一个每个成员的对齐方式正确。因为标准对齐通常是 2 的幂,根据经验,联合将在适合需要最大对齐的元素的边界上对齐。

【讨论】:

  • 会造成内存损坏。不一定。它也可能以其他方式失败。
  • @AndrewHenle 当然,但是在提供的示例中,int 应该大于char,这取决于机器默认对齐方式,很有可能覆盖下一个分配.
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-26
  • 1970-01-01
  • 1970-01-01
  • 2013-08-03
  • 2021-12-18
  • 2018-10-01
  • 1970-01-01
相关资源
最近更新 更多