首页 > 解决方案 > C 中具有严格别名和严格对齐的面向对象模式的最佳实践

问题描述

多年来,我一直在编写嵌入式 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;
}

根据 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:结构的联合

修改此代码的另一种方法是使用联合。所以这个print_val()函数可以重写如下:

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

    u = (union Ptr *)bp;
...

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

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:指针联合

如下重写函数可以在 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 上的一篇较早的别名文章特别指出类似的构造是无效的(请参阅通过联合进行铸造(3 )在该链接中)。

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

具体问题

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

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

提前致谢!

更新:

使用中间体void*可能是做到这一点的“正确”方式:

struct Derived1 *dp = (void*)bp;

这当然有效,但它确实允许任何转换,无论类型兼容性如何(我认为 C 的较弱类型系统基本上是这个原因,我真正想要的是 C++ 和static_cast<>运算符的近似值)

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

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

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

标签: cpointersstrict-aliasing

解决方案


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

struct Derived1 *dp = (struct Derived1 *) (void *) bp;

(在转换为 之后,在上述声明中void *转换为是自动的,因此您可以删除转换。)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 **,然后*形成一个 type 的左值struct Derived1 *。然后使用表达式来访问 astruct Base *的类型struct Derived1 *。别名规则中没有匹配项;它列出的用于访问 a 的类型都不struct Base *是 a struct Derived1 *


推荐阅读