这种联合的使用是否严格符合要求?
Is this use of unions strictly conforming?
给定代码:
struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };
static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}
int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
if (read_s1x(&p1->v1))
{
unsigned short temp;
temp = p3->v1.x;
p3->v2.x = temp;
write_s2x(&p2->v2,1234);
temp = p3->v2.x;
p3->v1.x = temp;
}
return read_s1x(&p1->v1);
}
int test2(int x)
{
union s1s2 q[2];
q->v1.x = 4321;
return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
printf("%d\n",test2(0));
}
整个程序中存在一个联合对象--q
。其活动成员设置为 v1
,然后设置为 v2
,然后再次设置为 v1
。当该成员处于活动状态时,代码仅在 q.v1
上使用地址运算符或结果指针,同样地 q.v2
。由于p1
、p2
、p3
都是同一类型,所以用p3->v1
访问p1->v1
、[=22=应该是完全合法的] 访问 p2->v2
.
我没有看到任何可以证明编译器无法输出 1234 的理由,但是包括 clang 和 gcc 在内的许多编译器生成输出 4321 的代码。我认为正在发生的事情是他们决定 p3 上的操作不会'实际上并没有改变内存中任何位的内容,它们可以被完全忽略,但我在标准中没有看到任何可以证明忽略 p3
用于从 [=21= 复制数据的事实] 到 p2->v2
,反之亦然。
标准中是否有任何内容可以证明这种行为是合理的,或者编译器是否根本不遵循它?
我相信您的代码是一致的,GCC 和 Clang 的 -fstrict-aliasing
模式存在缺陷。
我找不到C标准的正确部分,但是在用C++模式为我编译你的代码时出现了同样的问题,我确实找到了C++标准的相关段落。
在 C++ 标准中,[class.union]/5 定义了在联合访问表达式上使用运算符 =
时发生的情况。 C++ 标准规定,当内置运算符 =
的成员访问表达式中涉及联合时,联合的活动成员将更改为表达式中涉及的成员(如果类型具有普通构造函数,但因为这是 C 代码,所以它确实有一个简单的构造函数)。
注意write_s2x
不能改变联合体的活跃成员,因为联合体不参与赋值表达式。您的代码不会假设会发生这种情况,所以没关系。
即使我使用放置 new
显式更改哪个联合成员处于活动状态,这应该是对编译器的提示,表明活动成员已更改,GCC 仍会生成输出 4321
的代码.
这看起来像是 GCC 和 Clang 的一个错误,假设活跃联合成员的切换不会在这里发生,因为他们没有认识到 p1
、p2
和 [=18= 的可能性] 都指向同一个对象。
GCC 和 Clang(以及几乎所有其他编译器)支持对 C/C++ 的扩展,您可以在其中读取联合的非活动成员(作为结果获得任何潜在的垃圾值),但仅如果您在涉及联合的成员访问表达式中执行此访问。 如果 v1
不是活动成员,则 read_s1x
不会在此特定于实现的规则下定义行为,因为联合不在成员访问表达式中。但因为 v1
是活跃成员,所以这无关紧要。
这是一个复杂的案例,我希望我的分析是正确的,因为我不是编译器维护者,也不是其中一个委员会的成员。
这与符合或不符合无关 - 它是优化之一 "traps"。您的所有数据结构都已被优化,并且您将相同的指针传递给优化后的数据,因此执行树被简化为值的简单 printf。
sub rsp, 8
mov esi, 4321
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
要更改它,您需要使此 "transfer" 函数易于产生副作用并强制执行真正的赋值。它将强制优化器不减少执行树中的那些节点:
int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3)
/* ....*/
main:
sub rsp, 8
mov esi, 1234
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
这是一个非常简单的测试,只是被人为地变得复杂了一点。
根据对标准的严格 解释,此代码可能不符合。让我们关注一下著名的§6.5p7:
的文字
An object shall have its stored value accessed only by an lvalue expression that has one of
the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the
object,
— a type that is the signed or unsigned type corresponding to a qualified version of the
effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its
members (including, recursively, a member of a subaggregate or contained union), or
— a character type.
(强调我的)
你的函数 read_s1x()
和 write_s2x()
在整个代码的上下文中执行 相反 我在上面 标记为粗体的内容。仅通过这一段,您可以得出这样的结论:不允许指向 union s1s2
的指针作为指向 struct s1
的指针的别名,但反之则不行。
当然,这种解释意味着如果您 "inline" 在 test()
中手动执行这些功能,则代码必须按预期工作。 i686-w64-mingw32
.
的 gcc 6.2 确实是这种情况
添加两个支持上述严格解释的论点:
虽然始终允许使用 char *
为任何指针添加别名,但字符数组不能使用任何其他类型作为别名。
考虑(此处无关)§6.5.2.3p6:
One special guarantee is made in order to simplify the use of unions: if a union contains
several structures that share a common initial sequence (see below), and if the union
object currently contains one of these structures, it is permitted to inspect the common
initial part of any of them anywhere that a declaration of the completed type of the union
is visible.
(再次强调我的)——典型的解释是 可见 意味着直接在相关函数的范围内,而不是 "somewhere in the translation unit" ... 所以此保证不包括一个函数,该函数采用指向 union
的成员之一的 struct
的指针。
我没读过标准,但在严格别名模式(即使用 -fstrict-alising
)中使用指针是很危险的。见 gcc online doc:
Pay special attention to code like this:
union a_union {
int i;
double d;
};
int f() {
union a_union t;
t.d = 3.0;
return t.i;
}
The practice of reading from a different union member than the one most recently written to (called type-punning
) is common. Even with -fstrict-aliasing
, type-punning is allowed, provided the memory is accessed through the union type. So, the code above works as expected. See Structures unions enumerations and bit-fields implementation. However, this code might not:
int f() {
union a_union t;
int* ip;
t.d = 3.0;
ip = &t.i;
return *ip;
}
Similarly, access by taking the address, casting the resulting pointer and dereferencing the result has undefined behavior, even if the cast uses a union type, e.g.:
int f() {
double d = 3.0;
return ((union a_union *) &d)->i;
}
The -fstrict-aliasing
option is enabled at levels -O2, -O3, -Os.
在第二个示例中找到类似的东西吗?
给定代码:
struct s1 {unsigned short x;};
struct s2 {unsigned short x;};
union s1s2 { struct s1 v1; struct s2 v2; };
static int read_s1x(struct s1 *p) { return p->x; }
static void write_s2x(struct s2 *p, int v) { p->x=v;}
int test(union s1s2 *p1, union s1s2 *p2, union s1s2 *p3)
{
if (read_s1x(&p1->v1))
{
unsigned short temp;
temp = p3->v1.x;
p3->v2.x = temp;
write_s2x(&p2->v2,1234);
temp = p3->v2.x;
p3->v1.x = temp;
}
return read_s1x(&p1->v1);
}
int test2(int x)
{
union s1s2 q[2];
q->v1.x = 4321;
return test(q,q+x,q+x);
}
#include <stdio.h>
int main(void)
{
printf("%d\n",test2(0));
}
整个程序中存在一个联合对象--q
。其活动成员设置为 v1
,然后设置为 v2
,然后再次设置为 v1
。当该成员处于活动状态时,代码仅在 q.v1
上使用地址运算符或结果指针,同样地 q.v2
。由于p1
、p2
、p3
都是同一类型,所以用p3->v1
访问p1->v1
、[=22=应该是完全合法的] 访问 p2->v2
.
我没有看到任何可以证明编译器无法输出 1234 的理由,但是包括 clang 和 gcc 在内的许多编译器生成输出 4321 的代码。我认为正在发生的事情是他们决定 p3 上的操作不会'实际上并没有改变内存中任何位的内容,它们可以被完全忽略,但我在标准中没有看到任何可以证明忽略 p3
用于从 [=21= 复制数据的事实] 到 p2->v2
,反之亦然。
标准中是否有任何内容可以证明这种行为是合理的,或者编译器是否根本不遵循它?
我相信您的代码是一致的,GCC 和 Clang 的 -fstrict-aliasing
模式存在缺陷。
我找不到C标准的正确部分,但是在用C++模式为我编译你的代码时出现了同样的问题,我确实找到了C++标准的相关段落。
在 C++ 标准中,[class.union]/5 定义了在联合访问表达式上使用运算符 =
时发生的情况。 C++ 标准规定,当内置运算符 =
的成员访问表达式中涉及联合时,联合的活动成员将更改为表达式中涉及的成员(如果类型具有普通构造函数,但因为这是 C 代码,所以它确实有一个简单的构造函数)。
注意write_s2x
不能改变联合体的活跃成员,因为联合体不参与赋值表达式。您的代码不会假设会发生这种情况,所以没关系。
即使我使用放置 new
显式更改哪个联合成员处于活动状态,这应该是对编译器的提示,表明活动成员已更改,GCC 仍会生成输出 4321
的代码.
这看起来像是 GCC 和 Clang 的一个错误,假设活跃联合成员的切换不会在这里发生,因为他们没有认识到 p1
、p2
和 [=18= 的可能性] 都指向同一个对象。
GCC 和 Clang(以及几乎所有其他编译器)支持对 C/C++ 的扩展,您可以在其中读取联合的非活动成员(作为结果获得任何潜在的垃圾值),但仅如果您在涉及联合的成员访问表达式中执行此访问。 如果 v1
不是活动成员,则 read_s1x
不会在此特定于实现的规则下定义行为,因为联合不在成员访问表达式中。但因为 v1
是活跃成员,所以这无关紧要。
这是一个复杂的案例,我希望我的分析是正确的,因为我不是编译器维护者,也不是其中一个委员会的成员。
这与符合或不符合无关 - 它是优化之一 "traps"。您的所有数据结构都已被优化,并且您将相同的指针传递给优化后的数据,因此执行树被简化为值的简单 printf。
sub rsp, 8
mov esi, 4321
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
要更改它,您需要使此 "transfer" 函数易于产生副作用并强制执行真正的赋值。它将强制优化器不减少执行树中的那些节点:
int test(union s1s2 *p1, union s1s2 *p2, volatile union s1s2 *p3)
/* ....*/
main:
sub rsp, 8
mov esi, 1234
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
ret
这是一个非常简单的测试,只是被人为地变得复杂了一点。
根据对标准的严格 解释,此代码可能不符合。让我们关注一下著名的§6.5p7:
的文字An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
— a character type.
(强调我的)
你的函数 read_s1x()
和 write_s2x()
在整个代码的上下文中执行 相反 我在上面 标记为粗体的内容。仅通过这一段,您可以得出这样的结论:不允许指向 union s1s2
的指针作为指向 struct s1
的指针的别名,但反之则不行。
当然,这种解释意味着如果您 "inline" 在 test()
中手动执行这些功能,则代码必须按预期工作。 i686-w64-mingw32
.
添加两个支持上述严格解释的论点:
虽然始终允许使用
char *
为任何指针添加别名,但字符数组不能使用任何其他类型作为别名。考虑(此处无关)§6.5.2.3p6:
One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible.
(再次强调我的)——典型的解释是 可见 意味着直接在相关函数的范围内,而不是 "somewhere in the translation unit" ... 所以此保证不包括一个函数,该函数采用指向
union
的成员之一的struct
的指针。
我没读过标准,但在严格别名模式(即使用 -fstrict-alising
)中使用指针是很危险的。见 gcc online doc:
Pay special attention to code like this:
union a_union {
int i;
double d;
};
int f() {
union a_union t;
t.d = 3.0;
return t.i;
}
The practice of reading from a different union member than the one most recently written to (called
type-punning
) is common. Even with-fstrict-aliasing
, type-punning is allowed, provided the memory is accessed through the union type. So, the code above works as expected. See Structures unions enumerations and bit-fields implementation. However, this code might not:
int f() {
union a_union t;
int* ip;
t.d = 3.0;
ip = &t.i;
return *ip;
}
Similarly, access by taking the address, casting the resulting pointer and dereferencing the result has undefined behavior, even if the cast uses a union type, e.g.:
int f() {
double d = 3.0;
return ((union a_union *) &d)->i;
}
The
-fstrict-aliasing
option is enabled at levels -O2, -O3, -Os.
在第二个示例中找到类似的东西吗?