通过指针访问会改变严格的别名语义吗?
Does access through pointer change strict aliasing semantics?
使用这些定义:
struct My_Header { uintptr_t bits; }
struct Foo_Type { struct My_Header header; int x; }
struct Foo_Type *foo = ...;
struct Bar_Type { struct My_Header header; float x; }
struct Bar_Type *bar = ...;
这个C代码("case one"):
foo->header.bits = 1020;
...实际上与此代码 语义 不同 ("case two"):
struct My_Header *alias = &foo->header;
alias->bits = 1020;
我的理解是应该不一样:
情况一认为赋值不能影响Bar_Type中的header。它仅被视为能够在其他 Foo_Type 实例中影响 header。
第二种情况,通过强制通过通用别名指针进行访问,将导致优化器意识到对于可能包含 struct My_Header
的任何类型,所有赌注都将被取消。它将与通过任何指针类型的访问同步。 (例如,如果你有一个 Foo_Type
指向实际上是 Bar_Type
的东西,它可以通过 header 访问并可靠地找出它有什么——假设这是 header 位可以告诉你的。)
这依赖于优化器没有得到 "smart" 并将情况二变回情况一。
您有两个包含 My_Header
的结构这一事实是转移注意力并使您的思维复杂化,而没有给 table 带来任何新的东西。您的问题可以在没有任何结构的情况下陈述和澄清(当然 My_Header
除外)。
foo->header.bits = 1020;
编译器清楚地知道要修改哪个对象。
struct My_Header *alias = &foo->header;
alias->bits = 1020;
这里同样如此:通过非常基本的分析,编译器确切地知道 alias->bits = 1020;
修改了哪个对象。
有趣的部分来了:
void foo(struct My_Header* p)
{
p->bits = 1020;
}
在这个函数中,指针 p
可以作为 My_header
类型的任何对象(或子对象)的别名。如果您有 N 个包含 My_header
个成员的结构,或者您是否有 none 个结构,这真的无关紧要。 My_Header
类型的任何对象都可能在此函数中被修改。
例如
// global:
struct My_header* global_p;
void foo(struct My_Header* p)
{
p->bits = 1020;
global_p->bits = 15;
return p->bits;
// the compiler can't just return 1020 here because it doesn't know
// if `p` and `global_p` both alias the same object or not.
}
为了让您相信 Foo_Type
和 Bar_Type
是转移注意力并且无关紧要,请查看此示例,该示例的分析与之前的案例相同,但两者都不涉及 Foo_Type
也不 Bar_type
:
// global:
struct My_header* gloabl_p;
void foo(struct Foo_Type* foo)
{
foo->header.bits = 1020;
global_p->bits = 15;
return foo->header.bits;
// the compiler can't just return 1020 here because it doesn't know
// if `foo.header` and `global_p` both alias the same object or not.
}
N1570 p5.6p7 的编写方式,访问结构或联合的单个成员的代码的行为只有在访问是使用字符类型的左值或通过调用库函数执行时才会定义 memcpy
.即使结构或联合具有 T
类型的成员,标准(故意恕我直言)也避免使用看似不相关的 T
类型的左值授予访问聚合存储部分的全面许可。目前,gcc 和 clang 似乎授予使用成员类型左值访问结构的全面许可,但不授予联合访问权限,但 N1570 p5.6p7 不要求这样做。它对两种聚合及其成员应用相同的规则。由于标准不授予使用不相关的成员类型左值访问结构的全面权限,并且授予此类权限会损害有用的优化,因此无法保证 gcc 和 clang 将继续使用不相关的成员类型左值进行此行为。
不幸的是,正如使用联合所证明的那样,gcc 和 clang 在识别不同类型的左值之间的关系方面非常差,即使一个左值很明显是从另一个左值派生出来的。给出类似的东西:
struct s1 {short x; short y[3]; long z; };
struct s2 {short x; char y[6]; };
union U { struct s1 v1; struct s2 v2; } unionArr[100];
int i;
标准中的任何内容都不会区分以下函数对的“别名”行为:
int test1(int i)
{
return unionArr[i].v1.x;
}
int test2a(int j)
{
unionArr[j].v2.x = 1;
}
int test2a(int i)
{
struct s1 *p = &unionArr[i].v1;
return p->x;
}
int test2b(int j)
{
struct s2 *p = &unionArr[j].v2;
p->x = 1;
}
它们都使用 int
类型的左值来访问与 struct s1
、struct s2
、union U
和 [=20= 类型的对象关联的存储],即使 int
没有被列为允许访问其中任何一个的类型。
虽然即使第一种形式也会调用 UB 似乎很荒谬,但如果人们认识到对超出标准中明确列出的访问模式的支持是实施质量问题,那么这应该不是问题。根据公布的基本原理,标准的作者认为编译器编写者会尝试产生高质量的实现,因此没有必要禁止“符合”的实现质量低到无用。在访问 union U
的成员 v2.x
的情况下,实现可能是“符合”的,但不能处理 test1a()
或 test2b()
,但仅在某种意义上实现可能是“符合”的,但除了某些特定的人为设计和无用的程序之外,无法正确处理任何东西。
不幸的是,虽然我认为标准的作者可能期望高质量的实现能够处理像 test2a()
/test2b()
以及 test1a()
/test1b()
,gcc 和 clang 都不可靠地支持它们模式(*)。别名规则的既定目的是避免强制编译器在没有证据的情况下允许别名,并且别名的可能性是“可疑的”[doubtful]。我没有看到任何证据表明他们打算高质量的编译器不会识别采用 unionArr[i].v1
地址并使用它的代码可能会访问与使用 unionArr[i]
的其他代码相同的存储(这当然,明显与 unionArr[i].v2
相关联)。然而,gcc 和 clang 的作者似乎认为不必考虑这些事情就可以成为高质量的实现。
(*) Given e.g.
int test(int i, int j)
{
if (test2a(i))
test2b(j);
return test2a(i);
}
neither gcc nor clang will recognize that if i==j, test2b(j) would access the same storage as test2a(i), even when though both would access the same element of the same array.
代码 bar->header.bits = 1020;
与 struct My_Header *alias = &bar->header; alias->bits = 1020;
完全相同。
严格的别名规则是根据通过lvalues访问对象:
定义的
6.5p7 An object shall have its stored value accessed
only by an lvalue expression that has one of the following
types:
唯一重要的是左值的类型,以及左值指定的对象的有效类型。不是你是否将左值推导的一些中间阶段存储在指针变量中。
注意:自以下文本发布以来,问题已被编辑。以下文本适用于 space 由 malloc
分配的原始问题,而不是截至 8 月 23 日的当前问题。
关于代码是否正确。您的代码相当于 N2013 rev 1571 中的 Q80 effective_type_9.c
,这是对现有 C 实现的调查,着眼于起草改进的严格别名规则。
Q80. After writing a structure to a malloc’d region, can its members be accessed via a pointer to a different structure type that has the same leaf member type at the same offset?
绊脚石是代码(*bar).header.bits = 1020;
是否只设置了int
位的有效类型;或整个 *bar
。因此,读取 (*foo).header.bits
是读取一个 int
,还是读取整个 *foo
?
仅读取 int
不会是严格的别名违规(可以将 int
读取为 int
);但是将 Bar_Struct
读为 Foo_Struct
将是违规行为。
本文的作者考虑为整个 *bar
设置有效类型的写入,尽管他们没有给出他们的理由,而且我在 C 标准中没有看到任何文本来支持那个立场。
在我看来,目前对于您的代码是否正确还没有明确的答案。
使用这些定义:
struct My_Header { uintptr_t bits; }
struct Foo_Type { struct My_Header header; int x; }
struct Foo_Type *foo = ...;
struct Bar_Type { struct My_Header header; float x; }
struct Bar_Type *bar = ...;
这个C代码("case one"):
foo->header.bits = 1020;
...实际上与此代码 语义 不同 ("case two"):
struct My_Header *alias = &foo->header;
alias->bits = 1020;
我的理解是应该不一样:
情况一认为赋值不能影响Bar_Type中的header。它仅被视为能够在其他 Foo_Type 实例中影响 header。
第二种情况,通过强制通过通用别名指针进行访问,将导致优化器意识到对于可能包含
struct My_Header
的任何类型,所有赌注都将被取消。它将与通过任何指针类型的访问同步。 (例如,如果你有一个Foo_Type
指向实际上是Bar_Type
的东西,它可以通过 header 访问并可靠地找出它有什么——假设这是 header 位可以告诉你的。)
这依赖于优化器没有得到 "smart" 并将情况二变回情况一。
您有两个包含 My_Header
的结构这一事实是转移注意力并使您的思维复杂化,而没有给 table 带来任何新的东西。您的问题可以在没有任何结构的情况下陈述和澄清(当然 My_Header
除外)。
foo->header.bits = 1020;
编译器清楚地知道要修改哪个对象。
struct My_Header *alias = &foo->header;
alias->bits = 1020;
这里同样如此:通过非常基本的分析,编译器确切地知道 alias->bits = 1020;
修改了哪个对象。
有趣的部分来了:
void foo(struct My_Header* p)
{
p->bits = 1020;
}
在这个函数中,指针 p
可以作为 My_header
类型的任何对象(或子对象)的别名。如果您有 N 个包含 My_header
个成员的结构,或者您是否有 none 个结构,这真的无关紧要。 My_Header
类型的任何对象都可能在此函数中被修改。
例如
// global:
struct My_header* global_p;
void foo(struct My_Header* p)
{
p->bits = 1020;
global_p->bits = 15;
return p->bits;
// the compiler can't just return 1020 here because it doesn't know
// if `p` and `global_p` both alias the same object or not.
}
为了让您相信 Foo_Type
和 Bar_Type
是转移注意力并且无关紧要,请查看此示例,该示例的分析与之前的案例相同,但两者都不涉及 Foo_Type
也不 Bar_type
:
// global:
struct My_header* gloabl_p;
void foo(struct Foo_Type* foo)
{
foo->header.bits = 1020;
global_p->bits = 15;
return foo->header.bits;
// the compiler can't just return 1020 here because it doesn't know
// if `foo.header` and `global_p` both alias the same object or not.
}
N1570 p5.6p7 的编写方式,访问结构或联合的单个成员的代码的行为只有在访问是使用字符类型的左值或通过调用库函数执行时才会定义 memcpy
.即使结构或联合具有 T
类型的成员,标准(故意恕我直言)也避免使用看似不相关的 T
类型的左值授予访问聚合存储部分的全面许可。目前,gcc 和 clang 似乎授予使用成员类型左值访问结构的全面许可,但不授予联合访问权限,但 N1570 p5.6p7 不要求这样做。它对两种聚合及其成员应用相同的规则。由于标准不授予使用不相关的成员类型左值访问结构的全面权限,并且授予此类权限会损害有用的优化,因此无法保证 gcc 和 clang 将继续使用不相关的成员类型左值进行此行为。
不幸的是,正如使用联合所证明的那样,gcc 和 clang 在识别不同类型的左值之间的关系方面非常差,即使一个左值很明显是从另一个左值派生出来的。给出类似的东西:
struct s1 {short x; short y[3]; long z; };
struct s2 {short x; char y[6]; };
union U { struct s1 v1; struct s2 v2; } unionArr[100];
int i;
标准中的任何内容都不会区分以下函数对的“别名”行为:
int test1(int i)
{
return unionArr[i].v1.x;
}
int test2a(int j)
{
unionArr[j].v2.x = 1;
}
int test2a(int i)
{
struct s1 *p = &unionArr[i].v1;
return p->x;
}
int test2b(int j)
{
struct s2 *p = &unionArr[j].v2;
p->x = 1;
}
它们都使用 int
类型的左值来访问与 struct s1
、struct s2
、union U
和 [=20= 类型的对象关联的存储],即使 int
没有被列为允许访问其中任何一个的类型。
虽然即使第一种形式也会调用 UB 似乎很荒谬,但如果人们认识到对超出标准中明确列出的访问模式的支持是实施质量问题,那么这应该不是问题。根据公布的基本原理,标准的作者认为编译器编写者会尝试产生高质量的实现,因此没有必要禁止“符合”的实现质量低到无用。在访问 union U
的成员 v2.x
的情况下,实现可能是“符合”的,但不能处理 test1a()
或 test2b()
,但仅在某种意义上实现可能是“符合”的,但除了某些特定的人为设计和无用的程序之外,无法正确处理任何东西。
不幸的是,虽然我认为标准的作者可能期望高质量的实现能够处理像 test2a()
/test2b()
以及 test1a()
/test1b()
,gcc 和 clang 都不可靠地支持它们模式(*)。别名规则的既定目的是避免强制编译器在没有证据的情况下允许别名,并且别名的可能性是“可疑的”[doubtful]。我没有看到任何证据表明他们打算高质量的编译器不会识别采用 unionArr[i].v1
地址并使用它的代码可能会访问与使用 unionArr[i]
的其他代码相同的存储(这当然,明显与 unionArr[i].v2
相关联)。然而,gcc 和 clang 的作者似乎认为不必考虑这些事情就可以成为高质量的实现。
(*) Given e.g.
int test(int i, int j) { if (test2a(i)) test2b(j); return test2a(i); }
neither gcc nor clang will recognize that if i==j, test2b(j) would access the same storage as test2a(i), even when though both would access the same element of the same array.
代码 bar->header.bits = 1020;
与 struct My_Header *alias = &bar->header; alias->bits = 1020;
完全相同。
严格的别名规则是根据通过lvalues访问对象:
定义的6.5p7 An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
唯一重要的是左值的类型,以及左值指定的对象的有效类型。不是你是否将左值推导的一些中间阶段存储在指针变量中。
注意:自以下文本发布以来,问题已被编辑。以下文本适用于 space 由 malloc
分配的原始问题,而不是截至 8 月 23 日的当前问题。
关于代码是否正确。您的代码相当于 N2013 rev 1571 中的 Q80 effective_type_9.c
,这是对现有 C 实现的调查,着眼于起草改进的严格别名规则。
Q80. After writing a structure to a malloc’d region, can its members be accessed via a pointer to a different structure type that has the same leaf member type at the same offset?
绊脚石是代码(*bar).header.bits = 1020;
是否只设置了int
位的有效类型;或整个 *bar
。因此,读取 (*foo).header.bits
是读取一个 int
,还是读取整个 *foo
?
仅读取 int
不会是严格的别名违规(可以将 int
读取为 int
);但是将 Bar_Struct
读为 Foo_Struct
将是违规行为。
本文的作者考虑为整个 *bar
设置有效类型的写入,尽管他们没有给出他们的理由,而且我在 C 标准中没有看到任何文本来支持那个立场。
在我看来,目前对于您的代码是否正确还没有明确的答案。