在C语言中,不透明类型可以在堆栈上分配

14
设计 C 接口时,常见的做法是只在公共接口(.h)中暴露用户程序需要知道的内容。因此,例如,如果用户程序不需要知道结构的内部组件,则应将其保留隐藏。这确实是一种良好的实践方法,因为结构的内容和行为可能会在未来发生变化,而不影响接口。
实现该目标的好方法是使用不完整类型。现在我们可以建立一个仅使用指向 opaqueType 的指针的接口,而无需用户程序了解 struct foo 的内部工作。
但有时,可能需要静态分配这样的结构,通常在堆栈上进行操作以提高性能和内存碎片问题。显然,通过上述结构,opaqueType 是不完整的,因此其大小未知,因此无法进行静态分配。
解决方法是分配“shell type”,例如: typedef struct { int faketable[8]; } opaqueType; 以上结构强制设定了大小和对齐方式,但并没有更深入地描述结构的真正内容。因此,它符合使类型“不透明”的目标。它基本上是有效的,但在某些情况下(例如 GCC 4.4),编译器会抱怨它违反了严格别名规则,从而生成有缺陷的二进制文件。
现在,我已经阅读了很多关于严格别名的东西,所以我想我现在理解了它的含义。问题是:是否有一种方法可以定义一个不透明类型,但仍然可以在堆栈上分配,而不会违反严格别名规则?
请注意,我尝试过本文中描述的联合方法,但它仍会生成相同的警告。请注意,Visual、Clang 和 GCC 4.6 及更高版本都没有抱怨并可以正常工作。

根据测试,问题只在以下情况下发生:

  • 私有和公共类型不同。我在.c文件中将公共类型强制转换为私有类型。如果它们是同一个联合的一部分,显然无关紧要。如果公共类型包含 char 也无妨。
  • 如果对私有类型的所有操作仅为读取,则没有问题。只有写入才会导致问题。
  • 我还怀疑仅自动内联的函数会遇到麻烦。
  • 该问题仅在-gcc 4.4设置为-O3时发生。 -O2则没有问题。

最后,我的目标是C90。如果确实没有选择,则使用C99。


@rost0031:不确定为什么其他编译器没有抱怨,可能是一些优化问题,但在我看来这确实是一种违规行为。 - too honest for this site
我相信这确实是违反规则的例子,因此当它被“盲目”应用时(似乎是gcc 4.4),它会导致问题。我还相信,在上述情况下,别名情况非常简单。因此,任何编译器都可以轻松检测到它,并采取适当的行动(因此gcc 4.6+、clang、visual等可能会做出不同的处理)。本问题针对第一类编译器。 - Cyan
不要使用“int”作为虚假类型;请使用“char”。这样可以避免严格的别名问题,对吧? - Jonathan Leffler
5
我寻找这个问题的良好解决方案已经有20年了,我得出结论:我在寻找技术解决社会问题。现在我不做任何聪明的事情,只是公开适当的结构并破坏触及它们的任何人。例如,通过在向后兼容的错误修复版本中随机重命名成员和移动内容。 - Art
1
关于不完整数据类型、封装、数据隐藏、动态链接/后期绑定和面向对象的动态数据结构方法的优秀文章/教程,请参见ANSI-C中的面向对象编程。虽然它是用C语言编写的,需要相当深入的C语言知识,但它值得花费精力去消化这些材料。它涵盖了大多数C书籍或教程中没有包含的许多主题,并直接涵盖了这里所涉及的不透明(不完整)数据类型。 - David C. Rankin
显示剩余4条评论
3个回答

2
您可以通过使用max_align_t来强制对齐,而且您可以使用char数组避免严格别名问题,因为char被明确允许别名任何其他类型。

可以这样写:

#include <stdint.h>
struct opaque
{
    union
    {
        max_align_t a;
        char b[32]; // or whatever size you need.
    } u;
};

如果您想支持没有max_align_t的编译器,或者知道实际类型的对齐要求,则可以使用任何其他类型作为a联合成员。

更新:如果您针对C11,则还可以使用alignas()

#include <stdint.h>
#include <stdalign.h>
struct opaque
{
    alignas(max_align_t) char b[32];
};

当然,您可以用任何您认为合适的类型来替换max_align_t。甚至是一个整数。
更新 #2:
那么,在库中使用这种类型将类似于:
void public_function(struct opaque *po)
{
    struct private *pp = (struct private *)po->b;
    //use pp->...
}

这种方式,由于你将指向char的指针类型转换为其他类型,因此不会违反严格别名规则。


1
这看起来是个好主意。它可能需要测试,因为我对char*例外严格别名规则的理解是它只能单向工作。参见:http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html#cast_to_char_pointer - Cyan
我刚刚使用union提案进行了一次测试;不幸的是,它没有起作用。或者说:编译器(gcc 4.4)仍然会生成警告(与之前相同)和有错误的二进制文件(段错误)。:( - Cyan
@Kevin:我不明白空格有什么关系。重要的是如果你分配给a成员会有影响。但是如果你只使用b,那么应该是安全的。 - rodrigo
@Cyan:也许你使用了 opaque o; private *p = (private*)&o?我认为这将违反严格的别名规则。请改用:opaque o; private *p = (private*)o.u.b;。如果仍然失败,也许你可以发布一个最小化的示例。 - rodrigo
2
char 可以别名任何类型,但反之不行。 struct private * 不允许别名 charchar 数组。这是一个常见的误解,请查阅资料。 - 2501
显示剩余4条评论

1
对我来说,这似乎是不应该做的事情。
具有不透明指针的重点在于隐藏实现细节。实际结构所分配的内存类型和对齐方式,或者库是否管理除所指向的数据之外的其他数据也是实现细节。
当然,你可以记录可能存在的一些东西,但C语言使用了这种方法(严格别名),你只能通过Rodrigo的答案(使用max_align_t)进行更多或更少的黑客攻击。根据规则,您无法通过接口知道特定编译器对实现中的实际结构施加什么样的约束(对于某些微不足道的微控制器,甚至内存类型都可能很重要),因此我认为这不能以真正跨平台的方式可靠地完成。

1
你所需要的是类似于C++中private访问控制的等价物,但是如你所知,没有这样的等价物存在。你提出的方法大致上是我会采取的方法。但是,我会使opaqueType对实现类型的内部组件不可见,因此我将被迫在内部组件中将其转换为真实类型。强制转换不应该生成您提到的警告。
虽然使用起来很麻烦,但是您可以定义一个接口,为不透明类型提供“堆栈分配”的内存,而不暴露大小结构。思路是实现代码负责堆栈分配,用户传入回调函数以获取分配类型的指针。
typedef struct opaqueType_raii_callback opqaueType_raii_callback;
struct opaqueType_raii_callback {
    void (*func)(opqaueType_raii_callback *, opqaueType *);
};
extern void opaqueType_raii (opaqueType_raii_callback *);
extern void opaqueType_raii_v (opaqueType_raii_callback *, size_t);


void opaqueType_raii (opaqueType_raii_callback *cb) {
    opaqueType_raii_v(cb, 1);
}

void opqaueType_raii_v (opaqueType_raii_callback *cb, size_t n) {
    opaqueType x[n];
    cb->func(cb, x);
}

上面的定义看起来有些深奥,但这是我通常实现回调接口的方式。

struct foo_callback_data {
    opaqueType_raii_callback cb;
    int my_data;
    /* other data ... */
};

void foo_callback_function (opaqueType_raii_callback *cb, opaqueType *x) {
    struct foo_callback_data *data = (void *)cb;
    /* use x ... */
}

void foo () {
    struct foo_callback_data data;
    data.cb.func = foo_callback_function;
    opaqueType_raii(&data.cb);
}

难以理解...我可能需要回来再读几遍... - Cyan
@Cyan:栈的结构如下(最长寿命的作用域在前):调用您的API的外部代码,opaqueType_raii()(可选,外部代码可以直接调用下一个函数),opaqueType_raii_v(),最后是附加到opaqueType_raii_callback结构的函数指针(由外部代码设置)。opaqueType_raii_v()堆栈帧包含不透明对象,并将指向它的指针作为参数传递给回调函数。在我看来,这是用大锤子来打蚊子,但应该可以工作。 - Kevin
@Kevin:不需要全局/静态状态或跳板。请看示例用法。 - jxh
当然可以,但如果有额外的 void* 成员,阅读起来会更容易(相对而言;这仍然是过度设计)。 - Kevin
@Kevin:对于那些已经习惯动态分派的人来说,实际上并没有真正提高可读性。此外,结果是必须跟随另一个指针,并且很可能会导致额外的数据缓存未命中。 - jxh
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接