我试图理解 C18 标准第 6.5 节第 6 和 7 段的确切含义。第 6 段定义了对象的“有效类型”,第 7 段部分规定“对象的存储值只能由具有以下类型之一的左值表达式访问…与对象的有效类型兼容的类型,…”。

下面的例子来自。函数

short g(int *p, short *q) {
  short z = *q; *p = 10; return z;
}

在以下上下文中调用:

union int_or_short { int x; short y; } u = { .y = 3 };
int *p = &u.x; // p points to the x variant of u
short *q = &u.y; // q points to the y variant of u
return g(p, q); // g is called with aliased pointers p and q

作者声称这表现出未定义的行为。通过考虑执行内联版本的代码可以解释这一点:

union int_or_short { int x; short y; } u = { .y = 3 };
int *p = &u.x; // p points to the x variant of u
short *q = &u.y; // q points to the y variant of u
// g(p, q) is called, the body is inlined
short z = *q; // u has variant y and is accessed through y -> OK
*p = 10; // u has variant y and is accessed through x -> bad

文章指出

赋值违反了有效类型的规则。指向的*p = 10内存区域包含一个联合,其变体为类型,但通过指向类型变体的指针进行访问。这会导致未定义的行为。pyshortxint

p如果我正确理解了这个论点,那么(或)指向的对象q的有效类型为short,并且最后一条语句是通过 类型的左值访问该对象int,这与 不兼容short,因此违反了上述 C 标准第 7 段。但是写入*p=10真的算作对对象存储值的访问吗?

有人可以参考标准中的语言解释一下为什么这段代码会表现出未定义的行为吗?

那么下面的代码怎么样:

union int_or_short { int x; short y; } u = { .y = 3 };
short z = u.y;
u.x = 10;

我一直认为这段代码完全合法(没有未定义的行为),但如果第一段代码违反了规则,为什么这段代码不违反规则?为什么会*p=10算作对对象存储值的访问,而u.x=10没有?这两种访问都是使用类型的左值进行的写入int


最佳答案
3

关于

union int_or_short { int x; short y; } u = { .y = 3 };
int *p = &u.x;
short *q = &u.y;

,你写道:

p如果我正确理解了这篇文章, (或)指向的对象q的有效类型是short

如果我们按照文章的意思来解释,即p不指向int,那么这种说法与 C 语言规范不一致。它似乎试图应用 C17 6.5/6 中的一条规则,即(粗略地说)对象的有效类型由其中存储的最新值的类型决定,但该规则不适用于此处。

根据规范,

访问对象存储值的有效类型是该对象的声明类型(如果有)。

(C17 6.5/6)

指针p指向u.x,其声明类型为 。 的存储空间与 的存储空间重叠int不会改变这一点。 这两个对象的存储空间与 的存储空间重叠也不会改变这一点u.xu.yu

关于通过存储的值确定类型的规则在该段后面,但它们仅适用于没有声明类型的对象。语言规范设想的唯一没有声明类型的对象是动态分配的对象。

这篇文章的作者可能混淆了分配的对象和通过指针访问的对象。分配的对象只能通过指针访问,但并非所有通过指针的访问都是针对分配的对象。有效类型的规则与对象本身的性质有关,而不是访问它的方式。

而且,

写入是否*p=10真的算作对对象存储值的访问?

[修订:]

是的。“访问”是规范中定义的术语,意思是“读取或修改对象的值”(C17 3.1/1)。但这不是问题。的有效类型
*p是它指向的对象的声明类型,u.xint。此外,如果与事实相反,我们谈论的是动态分配的对象,那么规范将对该分配做出如下说明(仍为第 6.5/6 段):

对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。

因此,即使分配的对象正在发挥作用,该分配也不会有问题。

当然,直接执行类似的分配而不是通过指针也没有问题。

7

  • 文章可在此处找到:。以下是确切的引述:“赋值违反了有效类型的规则。指向的*p=10内存区域包含一个联合,其变体为类型,但通过指向类型变体的指针进行访问。这会导致未定义的行为。”pyshortxint


    – 


  • @SteveSiegel,这个答案回答了提出的问题。但是,你应该仔细阅读 Eric 的回答:他提出了一个有说服力的论点,即这篇文章并不打算提出你所说的主张。


    – 

  • 我觉得这个论点没有说服力,但我编辑了这个问题,包括原始版本和内联版本。我看不出内联有什么区别。你认为你关于没有未定义行为的论点也适用于原始代码吗?


    – 

  • @SteveSiegel,根据你对问题的更新,我认为你没有理解 Eric 的评论,其核心观点是,你所询问的代码(他认为)首先不应该根据 C 语言规范进行分析。相反,它应该被理解为使用 C 语法作为伪代码来说明关于前面非内联版本的主张。我确实认为这篇文章是有缺陷的,但它并不一定打算提出你所归因于它的特定缺陷主张。尽管如此,这个问题仍然可以按提出的方式回答,就像我所做的那样。


    – 


  • 让我们忘记内联版本,它可能被视为伪代码,也可能不被视为伪代码。只需关注原始版本,其中包括函数的定义g和对的特定调用g。你相信程序有未定义的行为吗?这就是作者的说法。


    – 

这篇论文的作者 Robbert Krebbers 并没有断言使用内联的 C 代码g具有未定义的行为。提供该代码只是为了向读者说明内联。

在评论中,您提供了(该链接应该在问题中提供)。阅读该论文时,Krebbers 谈到了通过将函数内联g到调用它的其他代码中来获取您显示的代码:

我们将内联 g 的部分函数体,以指示示例执行过程中别名指针的错误使用。

这种转换混淆了语义——当论文显示这段带有g内联的新代码时,并没有说明这段新的 C 代码是否打算按原样重新解释,即在g编译内联代码时可以看到联合定义,或者这种内联只是为了向用户展示当代码(g带有其原始的原位解释)移动到g调用的位置时会发生什么。换句话说,在后一种情况下,Krebbers 在函数内联后显示的新代码仅仅是内联如何改变代码的伪代码说明;它不是要编译的实际 C 代码。

原始函数g具有参数int *p, short *q,并且没有可见的联合声明。在这种情况下,编译器可能会假设*p*q不互斥。在调用时g,联合声明是可见的,并且显然u.xu.y互斥。如果我们采用上面的后一种假设,即原地g解释,那么一切都是一致的:

  • 编译时g,编译器可能会假设*p*q产生别名。
  • 使用会导致别名的指针进行调用是错误的g。尽管在调用点,编译器可以看到别名*p但在之前不可见的上下文中对其进行了分析。*qg
  • 的内联g将 的分析语义g(例如, 的编译器内部表示g)带入调用站点,而无需重新解释 的源代码g

鉴于此,当 Krebbers 说*p = 10;中的内联代码违反“有效类型规则”(实际上是 C 2018 6.5 7,通常称为别名规则)时,他们是说 中的代码g与传递给它的指针的组合违反了别名规则。这是正确的。Krebbers 只是说 中的代码g具有未定义的行为,而不是说明性内联代码如果重新编译将具有未定义的行为。

17

  • 我倾向于接受对这篇文章的这种解释,即它并不打算提出问题中归因于它的主张。


    – 

  • 我不明白为什么“编译器可能假设 *p 和 *q 不互为别名”。编译器可能假设这一点的唯一原因是,如果它们互为别名,则会出现未定义的行为。该论文认为存在未定义的行为,因为“赋值 *p=10 违反了有效类型的规则。p 指向的内存区域包含一个联合,其变体是 short 类型的 y,但通过指向 int 类型的变体 x 的指针进行访问。这会导致未定义的行为。”该论点适用于带有函数的原始代码g和我引用的内联版本。


    – 

  • 但是,这篇文章似乎仍然做出了不正确的声明。特别是:“指向的内存区域包含一个联合,其变体类型为,但通过指向类型为的变体的指针进行访问。这会导致未定义的行为。 ” C 允许访问与上次写入不同的联合成员。我认为,即使是 C++ 更严格的规则也允许pyshortxint写入与上次写入不同的成员。


    – 


  • 我编辑了这个问题,添加了论文链接和两个版本(原始版本和内联版本)的代码。我认为内联对论点没有任何影响。我没有在标准中看到任何可以区分原始版本和内联版本的内容。


    – 

  • @SteveSiegel:关于“我不明白为什么“编译器可能假设 *p 和 *q 不互斥”。编译器可能假设这一点的唯一原因是,如果它们互斥,则会出现未定义的行为。”:是的,这就是原因。要么*p*q不互斥,要么行为未定义。如果它们不互斥,则编译器的行为可能就像它们不互斥一样。如果它们互斥,则行为未定义,在这种情况下,任何行为都符合 C 标准 (none) 的要求,包括表现得好像它们不互斥一样。


    – 

缺陷报告 028 是在 C89 和 C99 之间编写的,目的是解决使用指向联合成员的指针的操作是否应具有与直接对联合成员执行的操作相同的语义的问题,并且毫无根据地指出,使用指针执行如果直接对联合成员执行则为实现定义的行为会引发未定义行为。我认为有效类型规则旨在说明,如果这些指针恰好标识联合成员,则具有标准定义而非实现定义行为的指针操作应该具有同样定义的行为,但它没有说明缺陷报告 028 旨在解决的实际情况。

一直以来,让基于类型的别名真正发挥作用都需要做到,但标准未能明确指出的是,它旨在表明编译器何时必须适应这样一种可能性:访问与通用类型的某些内容没有新的可见关系,可能会产生别名。给定代码:

useFloatPtr(&someUnion.floatMember);
useUnsignedPtr(&someUnion.unsignedMember);
useFloatPtrAgain(&someUnion.floatMember);

一个不故意视而不见的编译器会毫不费力地注意到传递给函数的所有三个指针都是从一个通用类型中新派生出来的。如果函数的处理独立于调用代码,那么编译器就没有理由期望传递的存储会在其他地方用作不同类型的东西,但也没有理由关心。如果函数是内联的,那么编译器应该能够看到在不同函数执行之间发生的不同指针类型的派生。

这与以下情况截然不同:

useFloatPtrAndUnsignedPtr(&someUnion.floatPtr, &someUnion.unsignedPtr);

为该函数生成代码的编译器可能有理由合并通过 的访问float*和通过 的访问unsigned*,但没有理由知道它不应该这样做。

在编写标准时,任何力求与现有程序和实践兼容的实现都需要支持以下用法:每个函数仅通过指向其成员之一的指针访问联合,无论它们这样做是因为无法内联函数,还是因为观察函数之间发生的联合成员指针类型的派生。在许多实现没有理由关心指针缺失的情况下,以要求实现识别指针派生的方式编写标准会很尴尬,而且在编译器编写者真诚努力支持现有实践的情况下,以及在编译器编写者没有做出如此真诚努力的情况下,花费笔墨也无济于事。

3

  • 感谢您提供有趣的背景信息。但您的结论是什么?根据现有的 C 标准,问题中的任何代码是否有未定义的行为?为什么或为什么不?


    – 

  • @SteveSiegel:根据现有标准,任何使用除字符类型或结构或联合类型以外的任何左值访问结构或联合的任何部分的代码都会调用 UB。如果theArray是结构或联合的任何非字符数组类型成员,则对格式的左值的每次评估或赋值structOrUnion.theArrayt[index]都会调用 UB。如果 UB 被认为可以容纳“如果有明显有用的事情,则执行该事情”的可能性,那么这不是问题,这是解释标准的唯一合理方式。


    – 

  • @SteveSiegel:Clang 和 gcc 假装标准允许任意使用指向结构成员类型的指针来访问这些结构成员,并将特定语法视为someUnion.arrayMember[index]唯一能够访问该数组元素的语法,但标准中没有任何内容禁止编译器将这两种构造都视为 UB,除了这样做会很明显地表明标准的作者并没有认真努力确保没有有用的构造被描述为 UB,而是依赖于编译器编写者想要变得有用的愿望。


    –