多年来,我一直在编写嵌入式 C 代码,新一代的编译器和优化在警告有问题的代码的能力方面确实变得更好了。
但是,至少有一个(根据我的经验非常常见)用例会继续引起悲痛,其中多个结构共享一个公共(public)基类型。考虑这个人为的例子:
#include <stdio.h>
struct Base
{
unsigned short t; /* identifies the actual structure type */
};
struct Derived1
{
struct Base b; /* identified by t=1 */
int i;
};
struct Derived2
{
struct Base b; /* identified by t=2 */
double d;
};
struct Derived1 s1 = { .b = { .t = 1 }, .i = 42 };
struct Derived2 s2 = { .b = { .t = 2 }, .d = 42.0 };
void print_val(struct Base *bp)
{
switch(bp->t)
{
case 1:
{
struct Derived1 *dp = (struct Derived1 *)bp;
printf("Derived1 value=%d\n", dp->i);
break;
}
case 2:
{
struct Derived2 *dp = (struct Derived2 *)bp;
printf("Derived2 value=%.1lf\n", dp->d);
break;
}
}
}
int main(int argc, char *argv[])
{
struct Base *bp1, *bp2;
bp1 = (struct Base*) &s1;
bp2 = (struct Base*) &s2;
print_val(bp1);
print_val(bp2);
return 0;
}
根据 ISO/IEC9899,上面代码中的转换应该没问题,因为它依赖于与包含结构共享相同地址的结构的第一个成员。第 6.7.2.1-13 条是这样说的:Within a structure object, the non-bit-field members and the units in which bit-fields
reside have addresses that increase in the order in which they are declared. A pointer to a
structure object, suitably converted, points to its initial member (or if that member is a
bit-field, then to the unit in which it resides), and vice versa. There may be unnamed
padding within a structure object, but not at its beginning.
从派生到基础的转换工作正常,但在 print_val()
内转换回派生类型生成对齐警告。然而,众所周知这是安全的,因为它特别是上述条款的“反之亦然”部分。问题是编译器根本不知道我们已经通过其他方式保证该结构实际上是其他类型的实例。当使用 gcc 版本 9.3.0 (Ubuntu 20.04) 使用标志编译时
-std=c99 -pedantic -fstrict-aliasing -Wstrict-aliasing -Wcast-align=strict -O3
我得到:alignment-1.c: In function ‘print_val’:
alignment-1.c:30:31: warning: cast increases required alignment of target type [-Wcast-align]
30 | struct Derived1 *dp = (struct Derived1 *)bp;
| ^
alignment-1.c:36:31: warning: cast increases required alignment of target type [-Wcast-align]
36 | struct Derived2 *dp = (struct Derived2 *)bp;
| ^
类似的警告出现在 clang 10 中。返工 1 : 指向指针的指针
在某些情况下用于避免对齐警告的方法(当已知指针对齐时,就像这里的情况一样)是使用中间指针到指针。例如:
struct Derived1 *dp = *((struct Derived1 **)&bp);
然而,这只是将对齐警告换成了严格的别名警告,至少在 gcc 上是这样:alignment-1a.c: In function ‘print_val’:
alignment-1a.c:30:33: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
30 | struct Derived1 *dp = *((struct Derived1 **)&bp);
| ~^~~~~~~~~~~~~~~~~~~~~~~~
如果将强制转换为左值,则同样如此,即:*((struct Base **)&dp) = bp;
也在 gcc 中发出警告。值得注意的是,只有 gcc 提示这个 - clang 10 似乎在没有警告的情况下接受了这一点,但我不确定这是否是故意的。
返工 2 : 结构 union
重新编写此代码的另一种方法是使用 union 。所以
print_val()
函数可以重写为:void print_val(struct Base *bp)
{
union Ptr
{
struct Base b;
struct Derived1 d1;
struct Derived2 d2;
} *u;
u = (union Ptr *)bp;
...
可以使用 union 访问各种结构。虽然这工作正常,但转换到 union 仍然被标记为违反对齐规则,就像原始示例一样。alignment-2.c:33:9: warning: cast from 'struct Base *' to 'union Ptr *' increases required alignment from 2 to 8 [-Wcast-align]
u = (union Ptr *)bp;
^~~~~~~~~~~~~~~
1 warning generated.
返工 3 : 指针 union 如下重写函数可以在 gcc 和 clang 中干净地编译:
void print_val(struct Base *bp)
{
union Ptr
{
struct Base *bp;
struct Derived1 *d1p;
struct Derived2 *d2p;
} u;
u.bp = bp;
switch(u.bp->t)
{
case 1:
{
printf("Derived1 value=%d\n", u.d1p->i);
break;
}
case 2:
{
printf("Derived2 value=%.1lf\n", u.d2p->d);
break;
}
}
}
关于这是否真的有效,似乎存在相互矛盾的信息。特别是在 https://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html 上的旧别名文章特别指出类似的构造无效(请参阅该链接中的通过 union (3) 进行类型转换)。在我的理解中,因为 union 的指针成员都共享一个共同的基类型,这实际上并没有违反任何别名规则,因为所有访问
struct Base
实际上将通过类型 struct Base
的对象完成- 是否通过取消引用 bp
union 成员或访问 b
d1p
的成员对象或 d2p
.无论哪种方式,它都通过类型 struct Base
的对象正确访问成员。 - 据我所知,没有别名。具体问题 :
在我看来,由于这种模式在 C 代码中相当普遍(在没有像 C++ 那样真正的 OO 构造的情况下),因此以可移植的方式执行此操作应该更直接,而不会以一种或另一种形式收到警告。
提前致谢!
更新:
使用中间件
void*
可能是执行此操作的“正确”方法:struct Derived1 *dp = (void*)bp;
这当然有效,但它确实允许任何转换,无论类型兼容性如何(我认为 C 的较弱类型系统应该为此负责,我真正想要的是 C++ 和 static_cast<>
运算符的近似值)但是,我关于严格别名规则的基本问题(误解?)仍然是:
为什么使用 union 类型和/或指针指向指针会违反严格的别名规则 ?换句话说,在 main 中所做的事情(取
b
成员的地址)和在 print_val()
中所做的事情之间有根本的不同。除了转换的方向?两者都产生相同的情况 - 指向同一内存的两个指针,它们是不同的结构类型 - struct Base*
和一个 struct Derived1*
.在我看来,如果这以任何方式违反了严格的别名规则,则引入中间
void*
cast 不会改变根本问题。
最佳答案
您可以通过强制转换为 void *
来避免编译器警告。第一的:
struct Derived1 *dp = (struct Derived1 *) (void *) bp;
(在转换为 void *
之后,转换为 struct Derived1 *
在上述声明中是自动的,因此您可以删除转换。)使用指向指针的指针或 union 来重新解释指针的方法不正确;他们违反了别名规则,如
struct Derived1 *
和一个 struct Base *
不适合相互别名的类型。不要使用那些方法。(由于 C 2018 6.2.6.1 28,它说“......所有指向结构类型的指针应具有彼此相同的表示和对齐要求......”,可以提出一个论点,将一个指向结构的指针重新解释为另一个C 标准支持通过 union 。脚注 49 说“相同的表示和对齐要求意味着作为函数的参数、函数的返回值和 union 的成员的可互换性。”然而,这充其量只是一个混杂的东西在 C 标准中,应尽可能避免。)
Why does using a union type and/or pointer-to-pointer violate strict aliasing rules? In other words what is fundamentally different between what is done in main (taking address of
b
member) and what is done inprint_val()
other than the direction of the conversion? Both yield the same situation - two pointers that point to the same memory, which are different struct types - astruct Base*
and astruct Derived1*
.It would seem to me that if this were violating strict aliasing rules in any way, the introduction of an intermediate
void*
cast would not change the fundamental problem.
严格别名违规发生在为指针设置别名时,而不是在为结构设置别名时。
如果您有
struct Derived1 *dp
或 struct Base *bp
然后你用它来访问内存中实际存在 struct Derived1
的地方或者,分别是 struct Base
,则不存在别名冲突,因为您通过其类型的左值访问对象,这是别名规则所允许的。但是,这个问题建议对指针使用别名。在
*((struct Derived1 **)&bp);
, &bp
是有struct Base *
的位置.这个地址是struct Base *
转换为 struct Derived1 **
的地址,然后 *
形成类型 struct Derived1 *
的左值.然后使用该表达式访问 struct Base *
使用 struct Derived1 *
的类型.在别名规则中没有匹配项;它没有列出用于访问 struct Base *
的任何类型是 struct Derived1 *
.
关于c - 在 C 中具有严格别名和严格对齐的面向对象模式的最佳实践,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66427774/