仅写入动态分配的内存块的一部分是否会设置整个块的有效类型?

Aar*_*nks 4 c strict-aliasing

编辑:我所说的“有效类型”在C11 Standard - \xc2\xa76.5 Expressions (p6,7)中提到。(感谢 David C. Rankin 在评论中提供此链接。)

\n\n

经过一些阅读后,我并不完全理解 C 中关于有效类型和严格别名的规则。我在下面的代码中评论了我认为有效类型所发生的情况。在本示例中,请假设 int 和 float 大小相同。

\n\n
void *memory = malloc(sizeof(int) + sizeof(float));\n\nint *x = memory;    // x points to an "object" with no effective type.\n*x = 1;             // x points to an object with effective type int.\nfloat *y = memory;  // y points to an object with effective type int.\n++y;                // y points to an "object" with effective type ???\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后,y指向尚未写入的内存。因此,如果 y 指向一个没有有效类型的“对象”,这对我来说是有意义的。

\n\n

另一方面,动态分配的“对象”中写入了一个 int,因此该“对象”可能会被解释为一个 int 数组。从这个角度来看,如果 y 指向一个有效类型为 int 的对象,这对我来说是有意义的。

\n\n

考虑另一个例子:

\n\n
void *memory = malloc(sizeof(short) + sizeof(float));\n\nshort *x = memory;  // x points to an "object" with no effective type.\n*x = 1;             // x points to an object with effective type short.\n++x;                // x points to an "object" with effective type ???\n
Run Code Online (Sandbox Code Playgroud)\n\n

在这里,由于内存对齐的原因,想象 x 指向浮点数似乎是不合理的。由于这样的对齐问题,我可以理解为什么写入内存块的一部分可能会设置整个块的有效类型。

\n\n

如果这总是正确的,如果我理解的话,从技术上讲,分配一个巨大的内存块并随后在其两端访问不同的数据类型将是未定义的行为。

\n\n

这确实是促使我研究有效类型的核心问题。我一直在使用自己的内存区域,但我无法弄清楚分配大块内存并将它们解释为连续打包的不同结构在技术上是否是错误的。它在实践中一直有效。否则,在动态分配的内存块中实现多种类型存储的有效方法是什么(除了将它们全部放在结构体或联合体中)?

\n

sup*_*cat 5

任何想要了解 C 标准的人都应该阅读已发布的基本原理文档(可在http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf上找到,这是第一个 google 搜索结果“ C99 基本原理”)。

支持各种有趣的结构的能力使 C 变得独特有用,一直是实施质量问题,超出了标准的管辖范围。相反,QoI 问题本来是要留给市场来解决的。由于客户需要某些结构的编译器编写者可能会寻求满足客户的需求,而不考虑标准是否需要它,因此不需要对某些程序需要但其他程序不需要的结构提供标准强制支持,也没有任何理由担心编写能够明确解决所有极端情况的规则。

您所询问的案例是该标准的作者似乎没有考虑的众多案例之一。因此,最合理的解释是,虽然该标准并不禁止实现无意义地处理此类构造,但它并不打算特别邀请这种行为,并且质量实现更关心构造是否有用,而不是它是否有用。授权的应该支持它。

有效类型规则基于对缺陷报告 028 的写得很糟糕的响应,该报告旨在解决编译器是否给出了类似以下内容的问题:

float test(float *p1, unsigned *p2)
{
  *p1 = 1.0f;
  *p2 = 0;
  return *p1;
}
Run Code Online (Sandbox Code Playgroud)

应该要求允许它可能被如下函数调用的可能性:

float test2(void)
{
  union { float f, unsigned u} uf;
  return test2(&uf.f, &uf.u);
}
Run Code Online (Sandbox Code Playgroud)

该响应正确地指出,不应要求编译器允许这种可能性,但引用了无意义的推理:因为将 an 写入unsigned联合对象并读取 an 的行为float是实现定义的行为,因此通过访问此类对象的行为指针是未定义的行为。没有给出任何依据来说明使用指针不应产生与直接使用对象相同的实现定义行为。这里的含义是,不具有联合的完全定义行为的操作将调用 UB。

事实上,对 DR #028 的正确响应应该是说,没有使用成员类型指针访问联合(甚至结构)成员的一般权限,但通过指针或左值进行访问可被识别为派生自出于类型访问规则的目的,应将可用于访问对象的不同类型之一视为通过原始类型的访问。编译器通常适应最常见的模式,其中代码派生并使用指针,但这种适应背后的实际机制各不相同。因此,编译器何时应容纳派生左值的问题被保留为实现质量问题。

有效类型规则试图通过编纂对 DR #028 的响应来“澄清”规则,却没有注意到它将实现定义的行为视为未定义的行为,而没有引用任何这样做的依据,而且它也完全没有考虑到许多重要的角落案例。结果,虽然这些规则据说是为了“澄清”事情,但实际上却产生了相反的效果。

从实际角度来看,clang 和 gcc 应被视为处理 C 的方言,它不允许通过任何特定非字符类型访问过的任何存储区域像任何其他区域一样可靠地访问,即使在以下情况下也是如此:标准将允许此类访问。相反,其他编译器(如 icc)会认识到,在它可以看到一种类型的指针或左值用于形成另一种类型的指针的情况下,对该指针的操作可能会影响原始对象,而不考虑标准是否要求它们注意这些事情。如果在块的生命周期内,malloc 块内存储的特定部分没有通过超过一种类型访问,则即使 clang 和 gcc 也可能允许使用不同类型访问块的不相交部分。然而,clang 和 gcc 都无法可靠地处理有时使用一种类型访问存储区域有时使用另一种类型的情况,即使用于形成对象地址的唯一指针已从旧类型转换为新类型。新型。