void** 是严格别名规则的例外吗?

Is void** an exception to strict aliasing rules?

基本上,当启用严格别名时,这段代码是否合法?

void f(int *pi) {
    void **pv = (void **) π
    *pv = NULL;
}

在这里,我们通过另一种类型的指针(指向void *的指针)访问一种类型(int*)的对象,所以我会说这确实是一个严格的别名违规.

但是一个试图突出未定义行为的样本让我怀疑(即使它不能证明它是合法的)。

首先,如果我们对 int *char * 进行别名,我们可以根据优化级别获得不同的值(因此这绝对是一个严格的别名违规):

#include <stdio.h>

static int v = 100;

void f(int **a, char **b) {
    *a = &v;
    *b = NULL;
    if (*a)
        // *b == *a (NULL)
        printf("Should never be printed: %i\n", **a);
}

int main() {
    int data = 5;
    int *a = &data;
    f(&a, (char **) &a);
    return 0;
}
$ gcc a.c && ./a.out
$ gcc -O2 -fno-strict-aliasing a.c && ./a.out
$ gcc -O2 a.c && ./a.out
Should never be printed: 100

但是使用 void ** 而不是 char ** 的同一个样本并没有表现出未定义的行为:

#include <stdio.h>

static int v = 100;

void f(int **a, void **b) {
    *a = &v;
    *b = NULL;
    if (*a)
        // *b == *a (NULL)
        printf("Should never be printed: %i\n", **a);
}

int main() {
    int data = 5;
    int *a = &data;
    f(&a, (void **) &a);
    return 0;
}
$ gcc a.c && ./a.out
$ gcc -O2 -fno-strict-aliasing a.c && ./a.out
$ gcc -O2 a.c && ./a.out

是偶然的吗?还是 void **?

的标准中有明确的例外?

或者也许只是编译器专门处理 void ** 因为实际上 (void **) &a 在野外太常见了?

Basically, is this code legal when strict aliasing is enabled?

没有。 pi 的有效类型是 int* 但你通过 void* 左值访问指针变量。取消引用指针以提供与对象的 有效类型 不对应的访问是一种严格的别名违规 - 除了某些例外,这不是一个。

在您的第二个示例中,函数的两个参数都设置为指向有效类型 int* 的对象,这是在此处完成的:f(&a, (char **) &a);。因此函数内部的 *b 确实是一个严格的别名违规,因为您正在使用 char* 类型进行访问。

在您的第三个示例中,您执行相同的操作,但使用 void*。这也是一个严格的混叠违规。在这种情况下,void*void** 没有什么特别之处。

为什么你的编译器在某些情况下表现出某种形式的未定义行为,推测起来意义不大。尽管 void* 根据定义必须可转换 to/from 任何其他对象指针类型,因此它们很可能在内部具有表示,即使这不是标准的明确要求。

您还使用了 -fno-strict-aliasing,它会关闭 各种基于指针别名的优化。如果你想引起奇怪和意想不到的结果,你不应该使用那个选项。

是的,void *char *是特殊的。

Is void** an exception to strict aliasing rules?

您没有通过 void ** 类型别名;你正在通过 void * 别名。在*pv = NULL中,*pv的类型是void *.

一般来说,C标准允许不同类型的指针有不同的表示。它们甚至可以有不同的尺寸。但是,它要求某些指针类型具有相同的表示形式。 C 2018 6.2.5 28 说 [为清楚起见,我将其分成要点]:

  • A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.49)
  • Similarly, pointers to qualified or unqualified versions of compatible types shall have the same representation and alignment requirements.
  • All pointers to structure types shall have the same representation and alignment requirements as each other.
  • All pointers to union types shall have the same representation and alignment requirements as each other.
  • Pointers to other types need not have the same representation or alignment requirements.

脚注 49 说:

The same representation and alignment requirements are meant to imply interchangeability as arguments to functions, return values from functions, and members of unions.

注释不属于标准的规范部分。也就是说,它不构成实现必须遵守的规则。但是,该注释似乎是在告诉我们,无论正式规则如何,您都应该能够在某些地方使用 void * 代替 char *,反之亦然。声明两件事应该可以互换看起来像是一条规则。我的解释是本文的作者打算 void *char * 可以互换,至少在某种程度上是这样,但没有适合放入 C 标准规范部分的正式措辞。 C标准对aliasing的处理其实是有缺陷的,比如,所以C标准确实需要重写规则

因此,虽然这不是标准的规范部分,但编译器开发人员可能会尊重它并支持将 char *void * 别名,反之亦然。这可以解释为什么您看到使用 char * 的别名表现得好像受支持,而使用 int * 的别名却没有。

虽然 char*void* 需要具有匹配的表示,但某些平台对 int* 使用不同的表示。因此,任何依赖于使用解除引用的 void** 可互换地访问所有指针类型的能力的代码都无法移植到此类机器,并且从标准的角度来看是“不可移植的”。因此,该标准放弃了对任何特定实现是否应支持此类构造的管辖权。这样做的实现将比不这样做的实现更适合低级编程,因此设计和配置为适合该目的的高质量实现将这样做。但是请注意,clang 和 gcc 都不是特别适合低级编程,除非使用 -fno-strict-aliasing 标志。

为了阐明为什么平台可能对 int*char* 使用不同的表示形式,一些硬件平台不允许直接寻址小于 16 位的块中的内存。该标准将允许此类平台的编译器以多种方式存储内容,在性能、存储效率和与期望 char 为 8 位的代码的兼容性之间进行不同的权衡:

  1. 只需让char匹配最小直接存储单元的大小(例如让charint都是16位)。我已经使用了一个编译器来做到这一点。这种方法可能会提供最佳性能,但是使用大型 unsigned char 数组来保存八位字节的代码会浪费其中一半的存储空间。

  2. 在每个 char 中存储 8 位有用数据,其余 8 位未使用。存储分为两个字的 16 位值和分为四个字的 32 位值。这将提供出色的兼容性,但性能和存储效率很差。

  3. char* 实现为一个指向 16 位字的指针的组合,一个指示它应该识别字的哪一半的位,以及 15 个填充位,但是实现 int* 作为指向 16 位字的简单指针。

  4. 如上实现char*,但在int*中添加一个填充字节。这会提高兼容性,但会浪费一些存储空间。

没有一种方法适合所有应用程序,但标准将允许实施 select 任何一种或多种方法(也许 select 可以通过命令行开关)对以下应用程序最有用他们的客户。