在 C 中具有严格别名和严格对齐的面向对象模式的最佳实践

Joe*_*key 8 c pointers strict-aliasing

多年来,我一直在编写嵌入式 C 代码,新一代的编译器和优化在警告有问题的代码的能力方面确实变得更好了。

但是,至少有一个(根据我的经验非常常见)用例会继续引起悲痛,其中多个结构共享一个公共基类型。考虑这个人为的例子:

#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;
}

Run Code Online (Sandbox Code Playgroud)

根据 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.
Run Code Online (Sandbox Code Playgroud)

从派生到基础的转换工作正常,但转换回派生类型会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;
      |                               ^
Run Code Online (Sandbox Code Playgroud)

类似的警告出现在 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);
      |                                ~^~~~~~~~~~~~~~~~~~~~~~~~
Run Code Online (Sandbox Code Playgroud)

如果将强制转换为左值,则同样如此,即:*((struct Base **)&dp) = bp;也在 gcc 中发出警告。

值得注意的是,只有 gcc 抱怨这个 - clang 10 似乎在没有警告的情况下接受了这一点,但我不确定这是否是故意的。

返工2:结构联合

重新编写此代码的另一种方法是使用联合。所以该print_val()函数可以重写为:

void print_val(struct Base *bp)
{
    union Ptr
    {
        struct Base b;
        struct Derived1 d1;
        struct Derived2 d2;
    } *u;

    u = (union Ptr *)bp;
...
Run Code Online (Sandbox Code Playgroud)

可以使用联合访问各种结构。虽然这工作正常,但转换到联合仍然被标记为违反对齐规则,就像原始示例一样。

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.
Run Code Online (Sandbox Code Playgroud)

返工3:指针的联合

如下重写函数可以在 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;
    }
    }
}
Run Code Online (Sandbox Code Playgroud)

关于这是否真的有效,似乎存在相互矛盾的信息。特别是,https: //cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html 上的一篇较旧的别名文章特别指出类似的构造无效(请参阅 Casting through a union (3 ) 在那个链接中)。

在我的理解中,因为联合的指针成员都共享一个共同的基类型,这实际上并没有违反任何别名规则,因为struct Base实际上所有访问都将通过类型的对象完成struct Base- 无论是通过取消引用bp联合成员还是通过访问or的b成员对象。无论哪种方式,它都是通过类型的对象正确访问成员- 据我所知,没有别名。d1pd2pstruct Base

具体问题

  1. 返工 3 中建议的指针联合是否是一种可移植、安全、符合标准、可接受的方法?
  2. 如果没有,是否有一个方法完全便携,符合标准的,不依赖于任何平台定义/编译器特定的行为或选择?

在我看来,由于这种模式在 C 代码中相当普遍(在没有像 C++ 那样真正的 OO 构造的情况下),因此以可移植的方式执行此操作应该更直接,而不会以一种或另一种形式收到警告。

提前致谢!

更新:

使用中间体void*可能是执行此操作的“正确”方法:

struct Derived1 *dp = (void*)bp;
Run Code Online (Sandbox Code Playgroud)

这当然有效,但它确实允许进行任何转换,无论类型兼容性如何(我认为 C 的较弱类型系统应该为此负责,我真正想要的是 C++ 和static_cast<>运算符的近似值)

但是,我关于严格别名规则的基本问题(误解?)仍然是:

为什么使用联合类型和/或指针指向指针会违反严格的别名规则?换句话说,在 main (获取b成员的地址)中所做的事情与在转换方向print_val()之外所做的事情之间有什么根本区别?两者都产生相同的情况 - 指向同一内存的两个指针,它们是不同的结构类型 - a和 a 。struct Base*struct Derived1*

在我看来,如果这以任何方式违反了严格的别名规则,引入中间void*转换不会改变根本问题。

Eri*_*hil 5

您可以通过强制转换为void *first来避免编译器警告:

struct Derived1 *dp = (struct Derived1 *) (void *) bp;
Run Code Online (Sandbox Code Playgroud)

(在void *转换为 to 之后struct Derived1 *,在上面的声明中转换为自动,因此您可以删除转换。)

使用指向指针的指针或联合来重新解释指针的方法不正确;它们违反了别名规则,因为 astruct Derived1 *和 astruct Base *不是相互别名的合适类型。不要使用那些方法。

(由于 C 2018 6.2.6.1 28,它说“......所有指向结构类型的指针应具有彼此相同的表示和对齐要求......”,可以提出一个论点,将一个指向结构的指针重新解释为另一个C 标准支持通过联合。脚注 49 说“相同的表示和对齐要求意味着作为函数的参数、函数的返回值和联合的成员的可互换性。”然而,这充其量只是一个混杂的东西在 C 标准中,应尽可能避免。)

为什么使用联合类型和/或指针指向指针会违反严格的别名规则?换句话说,在 main (获取b成员的地址)中所做的事情与在转换方向print_val()之外所做的事情之间有什么根本区别?两者都产生相同的情况 - 指向同一内存的两个指针,它们是不同的结构类型 - a和 a 。struct Base*struct Derived1*

在我看来,如果这以任何方式违反了严格的别名规则,引入中间void*转换不会改变根本问题。

严格别名违规发生在为指针设置别名时,而不是在为结构设置别名时。

如果您有 astruct Derived1 *dp或 astruct Base *bp并且您使用它来访问内存中实际存在 astruct Derived1或 a 的位置struct Base,则不存在别名冲突,因为您是通过其类型的左值访问对象,这是允许的别名规则。

但是,这个问题建议对指针使用别名。在*((struct Derived1 **)&bp);,&bp是存在 的位置struct Base *。这个 astruct Base *的地址被转换为 a 的地址struct Derived1 **,然后*形成一个类型的左值struct Derived1 *。然后使用该表达式来访问 astruct Base *使用的类型struct Derived1 *。在别名规则中没有匹配项;它列出的用于访问 a 的类型都不struct Base *是 a struct Derived1 *