12

在设计 C 接口时,通常只让.h用户程序知道的内容进入公共接口 ( )。

因此,例如,如果用户程序不需要知道结构的内部组件,则它们应该保持隐藏。这确实是一种很好的做法,因为结构的内容和行为将来可能会发生变化,而不会影响界面。

实现该目标的一个好方法是使用不完整类型。

typedef struct foo opaqueType;

现在可以构建一个只使用指针的接口opaqueType,而用户程序不需要知道struct foo.

但有时,可能需要静态分配此类结构,通常在堆栈上,以解决性能和内存碎片问题。显然,上面的构造,opaqueType是不完整的,所以它的大小是未知的,所以它不能被静态分配。

一种解决方法是分配“外壳类型”,例如:

typedef struct { int faketable[8]; } opaqueType;

上面的构造强制了大小和对齐,但没有进一步描述结构真正包含的内容。所以它符合保持类型“不透明”的目标。

它主要工作。但在一种情况下(GCC 4.4),编译器抱怨它破坏了严格的别名,并生成了错误的二进制文件。

现在,我已经阅读了大量关于严格别名的内容,所以我想我现在明白它的含义了。

问题是:有没有办法定义一个不透明的类型,它仍然可以在堆栈上分配,并且不违反严格的别名规则?

请注意,我已经尝试过这篇优秀文章中描述的联合方法,但它仍然会产生相同的警告。

另请注意,视觉、clang 和 gcc 4.6 及更高版本不会抱怨并且可以正常使用此构造。

[编辑]信息补充:

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

  • 私有和公共类型不同。我在.c文件中将公共类型转换为私有。他们是否属于同一个工会显然并不重要。公共类型是否包含char.
  • 如果私有类型的所有操作都只是读取,那没有问题。只有写入会导致问题。
  • 我还怀疑只有自动内联的函数才会遇到麻烦。
  • 问题仅发生在 gcc 4.4 的 -O3 设置上。-O2 很好。

最后,我的目标是C90。如果真的别无选择,也许是C99。

4

3 回答 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您的指针进行类型双关,因此不会违反严格的别名规则。

于 2015-07-02T23:12:25.053 回答
1

您想要的是某种与 C 中的 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);
}
于 2015-07-02T23:02:17.347 回答
1

对我来说,这似乎是不应该做的事情。

使用不透明指针的目的是隐藏实现细节。分配实际结构的内存的类型和对齐方式,或者库是否管理超出所指内容的附加数据也是实现细节。

当然,并不是说您无法记录某件事或另一件事是可能的,而是 C 语言使用这种方法(严格别名),您只能或多或少地通过 Rodrigo 的回答(使用max_align_t)来破解。根据规则,您无法通过接口知道特定编译器会对实现中的实际结构施加什么样的约束(对于某些深奥的微控制器,甚至内存的类型也可能很重要),所以我认为这不能以真正跨平台的方式可靠地完成。

于 2016-08-22T05:02:40.100 回答