与严格别名相关的有效类型规则

Effective type rules with relation to strict aliasing

所以,在过去的几天里,我一直在努力反对严格的别名规则和有效的类型规则。虽然它的精神很清楚,但我想确定对规则的良好技术理解。请注意,我已经解决了很多关于 SO 的相关问题,但我不认为这里提出的问题已经以在任何其他地方真正与我同在的方式得到了回答。

这个问题分为两部分。

第一部分,我把有效的类型规则分成了句子,并解释了我自己对每一个的理解。对于其中的每一个,请验证我的理解是否正确,如果有缺陷请纠正我并解释原因。对于最后一个 "sentence",我还提出了两个问题,希望得到解答。

问题的第二部分是我对特区的理解。

第 1 部分:有效类型规则

句子 1

The effective type of an object for an access to its stored value is the declared type of the object, if any.

这很清楚 - 一个声明的对象,如 int x 有一个永久有效类型,这是它声明的类型(在本例中为 int)。

句子 2

If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value.

一个"object having no declared type"一般是一个动态分配的对象。

当我们将数据存储在分配的对象中时(无论它是否已经具有有效类型)对象的有效类型成为用于访问的左值类型用于存储的数据(除非左值是字符类型)。例如:

int* x = malloc(sizeof(int)); // *x has no effective type yet
*x = 10; // *x has effective type int, because the type of lvalue *x is int

也可以更改已有有效类型的对象的有效类型。例如:

float* f = (float*) x;
*f = 20.5; // *x now has effective type float, because the type of lvalue *f is float.

句子 3

If a value is copied into an object having no declared type using memcpy or memmove, or is copied as an array of character type, then the effective type of the modified object for that access and for subsequent accesses that do not modify the value is the effective type of the object from which the value is copied, if it has one.

这意味着当我们将一个值设置到分配的对象中时,如果该值是通过与 char* 兼容类型的左值设置的(或通过 memcpymemmove) ,对象的有效类型成为复制到其中的数据的有效类型。例如:

int* int_array = malloc(sizeof(int) * 5); // *int_array has no effective type yet
int other_int_array[] = {10, 20, 30, 40, 50};
char* other_as_char_array = (char*) other_int_array;
for (int i = 0; i < sizeof(int) * 5; i++) {
    *((char*) int_array + i) = other_as_char_array[i];
}
// *int_array now has effective type int

句子4

For all other accesses to an object having no declared type, the effective type of the object is simply the type of the lvalue used for the access.

关于这部分我有两个问题:

一个。通过“For all other accesses”,文本是否仅表示 "for all read accesses"?

在我看来,所有引用未声明类型对象的先前规则都只处理存储一个值。那么这仅仅是针对未声明类型的对象(可能已经或可能没有有效类型)的任何 read 操作的规则吗?

乙。内存中的特定对象只有一种有效类型。那么 - "For all other accesses" 的文本是什么意思...这不是访问的问题,而是对象的 objective 有效类型的问题。不是吗?请说明文本的语言。

第 2 部分:关于严格别名的问题

严格的别名规则描述是这样开始的(强调我的):

An object shall have its stored value accessed only by an lvalue expression that has one of the following types [...]

当文字说 "stored value accessed" - 它是指读写访问,还是只读?

作为问这个问题的另一种方式:以下代码是否构成严格的别名违规或是否合法?

int* x = malloc(sizeof(int)); // *x - no effective type yet
*x = 8; // *x - effective type int
printf("%d \n", *x); // access the int object through lvalue *x

float* f = (float*) x; // casting itself is legal
*f = 12.5; // effective type of *x changes to float - *** is this a SAR violation? ***
printf("%g \n", *f); // access the float object through lvalue *f

"access"表示读或写。 "For all other accesses" 表示该段中尚未涵盖的任何访问。回顾一下,已涵盖的对未声明类型的对象的访问是:

  • 一个值被存储到一个没有声明类型的对象中 类型不是字符类型的左值,
  • 不修改存储值的后续访问
  • 一个值被复制到一个没有声明类型的对象中 memcpy 或 memmove
  • 或复制为字符型数组

所以 "all read and writes" 的剩余情况是:

  • 一个值通过左值存储到一个没有声明类型的对象中,该左值的类型字符类型,
  • 我们没有想到的任何其他写作

第 2 部分的代码根据 C11 的文本是正确的:

If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access

*x = 8; 通过类型不是字符类型的左值将值存储到没有声明类型的对象中。所以这个访问对象的有效类型是 int,然后在 6.5/7 中我们有有效类型 int 的对象被类型 int 的左值访问。同样的推理适用于 *f = 20.5float 而不是 int

脚注:有很多理由相信 6.5/6 和 /7 的文本是有缺陷的,正如您在搜索有关该主题的其他问题时所看到的那样。人们(和编译器编写者)对规则形成了自己的解释。

据我所知,委员会成员之间从未就 "Effective Type" 规则在所有极端情况下的含义达成共识;任何似是而非的解释都会禁止应该有用的优化,无法容纳应该可用的构造,或两者兼而有之。据我所知,没有哪个编译器能像 clang 和 gcc 那样 "strict" 以符合标准的任何合理解释的方式正确处理规则提出的所有极端情况。

struct s1 { char x[1]; };
struct s2 { char x[1]; };

void convert_p_to_s1(void *p)
{
    int q = ((struct s2*)p)->x[0]+1;
    ((struct s1*)p)->x[0] = q-1;
}

int test(struct s1 *p1, struct s2 *p2)
{
    p1->x[0] = 1;
    p2->x[0] = 2;
    convert_p_to_s1(p1);
    return p1->x[0];
}

clang 和 gcc 都不允许 test 可能将 struct s1 的成员 x[0] 写入某个位置,然后使用成员 [=12= 写入相同位置的可能性] 的 struct s2,然后使用 x[0]struct s2 读取,使用 struct s1x[0] 写入,然后使用 x[0] 的读取struct s1,所有读取和写入都是通过取消引用指针类型 char* 执行的,并且每次读取从结构指针派生的左值之前,都会先通过以相同方式派生的左值写入该存储来自相同类型的指针。

在 C99 之前,人们几乎普遍认为质量实现应该避免以对其客户有害的方式应用类型访问规则,而不考虑标准是否需要这种限制。因为一些实现用于需要能够以奇怪的方式访问对象但不需要花哨的优化的目的,而其他实现用于不需要以棘手的方式访问存储但需要更多优化的目的,问题究竟何时实现应该认识到对一个对象的访问可能会影响另一个对象被留作实现质量问题。

然而,C99 的一些作者可能反对这样一个事实,即规则实际上并不要求实现支持所有实现都应支持的构造,事实上几乎所有实现都已经支持。为了解决他们认为的缺陷,他们添加了一些额外的规则,这些规则将强制支持他们认为所有实施都应该支持的一些构造,并且故意不强制支持一些不需要普遍支持的构造。然而,他们似乎没有做出任何重大努力来考虑极端情况以及规则是否会明智地处理它们。

如果作者愿意承认某些任务比其他任务需要更强的保证,并且预期用于不同类型任务的实现应该支持不同的,那么标准可以说出关于指针别名的任何有用信息的唯一方法与这些任务相适应的保证。否则,C 应该被视为两个方言家族——其中一个要求任何使用特定类型访问过的存储在其生命周期内永远不能使用任何其他类型访问,另一个承认对目标的操作从另一种类型的指针新鲜可见地派生的指针可能会影响由原始指针标识的对象。