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 位的代码的兼容性之间进行不同的权衡:
只需让char
匹配最小直接存储单元的大小(例如让char
和int
都是16位)。我已经使用了一个编译器来做到这一点。这种方法可能会提供最佳性能,但是使用大型 unsigned char
数组来保存八位字节的代码会浪费其中一半的存储空间。
在每个 char
中存储 8 位有用数据,其余 8 位未使用。存储分为两个字的 16 位值和分为四个字的 32 位值。这将提供出色的兼容性,但性能和存储效率很差。
将 char*
实现为一个指向 16 位字的指针的组合,一个指示它应该识别字的哪一半的位,以及 15 个填充位,但是实现 int*
作为指向 16 位字的简单指针。
如上实现char*
,但在int*
中添加一个填充字节。这会提高兼容性,但会浪费一些存储空间。
没有一种方法适合所有应用程序,但标准将允许实施 select 任何一种或多种方法(也许 select 可以通过命令行开关)对以下应用程序最有用他们的客户。
基本上,当启用严格别名时,这段代码是否合法?
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的处理其实是有缺陷的,比如
因此,虽然这不是标准的规范部分,但编译器开发人员可能会尊重它并支持将 char *
与 void *
别名,反之亦然。这可以解释为什么您看到使用 char *
的别名表现得好像受支持,而使用 int *
的别名却没有。
虽然 char*
和 void*
需要具有匹配的表示,但某些平台对 int*
使用不同的表示。因此,任何依赖于使用解除引用的 void**
可互换地访问所有指针类型的能力的代码都无法移植到此类机器,并且从标准的角度来看是“不可移植的”。因此,该标准放弃了对任何特定实现是否应支持此类构造的管辖权。这样做的实现将比不这样做的实现更适合低级编程,因此设计和配置为适合该目的的高质量实现将这样做。但是请注意,clang 和 gcc 都不是特别适合低级编程,除非使用 -fno-strict-aliasing
标志。
为了阐明为什么平台可能对 int*
和 char*
使用不同的表示形式,一些硬件平台不允许直接寻址小于 16 位的块中的内存。该标准将允许此类平台的编译器以多种方式存储内容,在性能、存储效率和与期望 char
为 8 位的代码的兼容性之间进行不同的权衡:
只需让
char
匹配最小直接存储单元的大小(例如让char
和int
都是16位)。我已经使用了一个编译器来做到这一点。这种方法可能会提供最佳性能,但是使用大型unsigned char
数组来保存八位字节的代码会浪费其中一半的存储空间。在每个
char
中存储 8 位有用数据,其余 8 位未使用。存储分为两个字的 16 位值和分为四个字的 32 位值。这将提供出色的兼容性,但性能和存储效率很差。将
char*
实现为一个指向 16 位字的指针的组合,一个指示它应该识别字的哪一半的位,以及 15 个填充位,但是实现int*
作为指向 16 位字的简单指针。如上实现
char*
,但在int*
中添加一个填充字节。这会提高兼容性,但会浪费一些存储空间。
没有一种方法适合所有应用程序,但标准将允许实施 select 任何一种或多种方法(也许 select 可以通过命令行开关)对以下应用程序最有用他们的客户。