【问题标题】:Is there a way to make GCC/Clang aware of inheritance in C?有没有办法让 GCC/Clang 意识到 C 中的继承?
【发布时间】:2014-01-17 10:29:02
【问题描述】:

我正在编写一个使用一些简单的面向对象继承的 C 库,就像这样:

struct Base {
    int x;
};

struct Derived {
    struct Base base;
    int y;
};

现在我想将 Derived* 传递给一个采用 Base* 的函数,就像这样:

int getx(struct Base *arg) {
    return arg->x;
};

int main() {
    struct Derived d;
    return getx(&d);
};

这可行,当然是类型安全的,但编译器不知道这一点。有没有办法告诉编译器这是类型安全的?我在这里只关注 GCC 和 clang,因此欢迎使用特定于编译器的答案。我对看到一些使用__attribute__((inherits(Base)) 或类似的东西执行此操作的代码有模糊的记忆,但我的记忆可能是在撒谎。

【问题讨论】:

  • 这可能是一个愚蠢的问题,但如果你真的想要 C++,为什么不使用 C++?而不是试图破解 C 做类似的事情,也就是说。这不像是同一个编译器不能通过开关翻转来处理两种语言。
  • @Damon 也许他没有可用的 C++ 编译器。如果他正在使用嵌入式设备,那么更改编译器可能绝非易事。
  • 但是 GCC 和 Clang 都是 C/C++ 编译器。他们会做任何事情,这取决于您在命令行上提供的语言标准(或者只是源文件的名称,如果您不告诉编译器其他任何内容)。
  • @Damon 可能还有其他原因。公司规则或与其他工具的兼容性。
  • @Damon:我正在添加一个更大的、预先存在的 C 代码库。我宁愿把它全部保留 C.

标签: c inheritance gcc clang


【解决方案1】:

这在 C 语言中是安全的,但您应该将参数强制转换为 Base *。禁止别名(或者更准确地说,将其排除在标准 C 中的支持)的规则在 C 2011 6.5 中,其中第 7 段指出:

对象的存储值只能由具有以下类型之一的左值表达式访问:

——与对象的有效类型兼容的类型,

此规则阻止我们获取指向float 的指针,将其转换为指向int 的指针,并取消引用指向int 的指针以将float 作为int 访问。 (更准确地说,它不会阻止我们尝试,但它会使行为未定义。)

您的代码似乎违反了这一点,因为它使用Base 左值访问Derived 对象。但是,C 2011 6.7.2.1 第 15 段状态支持将指向 Derived 的指针转换为指向 Base 的指针:

…一个指向结构对象的指针,经过适当转换,指向它的初始成员…

所以,当我们将指向Derived 的指针转换为指向Base 的指针时,我们实际上拥有的不是指向Derived 对象的指针,它使用了不同的类型(这是被禁止的),而是一个使用 Base 的实际类型指向 Derived 对象的第一个成员的指针,这非常好。

关于编辑:最初我声明函数参数将转换为参数类型。但是,C 6.5.2.2 2 要求每个参数都有一个类型,该类型可以分配给具有其相应参数类型的对象(删除任何限定符,如 const),而 6.5.16.1 要求在分配一个指针时对于另一个,它们具有兼容的类型(或满足此处不适用的其他条件)。因此,将指向Derived 的指针传递给接受指向Base 的指针的函数违反了标准C 约束。但是,如果您自己执行转换,则它是合法的。如果需要,可以将转换构建到调用该函数的预处理器宏中,这样代码仍然看起来像一个简单的函数调用。

【讨论】:

  • 但是“对象的有效类型 ...是对象的声明类型,如果有的话”,声明的类型是Derived。 GCC 也无法编译它,因为 Base*Derived* 不是兼容的指针类型。 Clang 也一样。
  • 我将“适当转换”解释为“转换为char*unsigned char*void*”——但也许我错了。
  • @larsmans: 不,声明的类型不是Derived,因为转换后的指针不是指向Derived结构的指针,而是指向第一个成员的指针,而第一个成员是Base。转换后的指针的类型为“指向Base 的指针”,它指向一个Base 对象。
  • @larsmans:你为什么声称 GCC 不编译这个?我获取了问题的来源,删除了函数定义后面不正确的分号,并使用cc -Wmost -pedantic -O3 -g -std=c99 将其与 Apple GNU C 4.2.1 一起编译,然后编译。有关于 getx 的参数类型的警告,但没有错误。
  • @larsmans:“适当转换”本质上意味着转换为指向成员类型的指针。当 C 标准说某事“指向一个 x”时,这意味着它是一个指向 x 类型的指针,并指向一个 x目的。当它想说指向字符类型的指针指向对象的第一个字节时,它说指针指向的是字节,而不是对象。
【解决方案2】:

给出基成员的地址(真正的类型安全选项):

getx(&d.base);

或者使用空指针:

int getx(void * arg) {
    struct Base * temp = arg;
    return temp->x;
};

int main() {
    struct Derived d;
    return getx(&d);
};

之所以有效,是因为 C 要求在第一个结构成员之前不能有填充。这不会增加类型安全性,但消除了强制转换的需要。

【讨论】:

  • 还是保持原样使用getx((void *)&d)吧?
  • @AlterMann 嗯,是的。或使用getx((struct Base *)&d)
  • 但是(void *)&d 是允许的吗?
  • @AlterMann 唯一的区别我看到转换为void * 并返回是隐式或显式的。这应该没有任何区别。
【解决方案3】:

正如上面 user694733 所指出的,您可能最好通过使用基字段的地址来符合标准和类型安全(重复以供将来参考)

struct Base{
   int x;
}
struct Derived{
   int y;
   struct Base b; /* look mam, not the first field! */
}

struct Derived d = {0}, *pd = &d;
void getx (struct Base* b);

现在尽管 base 不是第一个字段,你仍然可以做

getx (&d.b); 

或者如果你正在处理一个指针

getx(&pd->b). 

这是一个很常见的成语。但是,如果指针为 NULL,则必须小心,因为 &pd->b 只是

(struct Base*)((char*)pd + offsetof(struct Derived, b)) 

所以 &((Derived*)NULL)->b 变成

((struct Base*)offsetof(struct Derived, b)) != NULL.

IMO 错过了 C 采用匿名结构但没有采用 plan9 匿名结构模型的机会,即

struct Derived{
    int y;
    struct Base; /* look mam, no fieldname */
} d;

它允许你只写 getx(&d) 并且编译器会将 Derived 指针调整为基指针,即它的含义与上面示例中的 getx(&d.b) 完全相同。换句话说,它有效地为您提供了继承,但具有非常具体的内存布局模型。特别是,如果您坚持不在顶部嵌入(== 继承)基本结构,则必须自己处理 NULL。正如您对继承所期望的那样,它以递归方式工作,因此对于

struct TwiceDerived{
    struct Derived;
    int z;
} td;

你仍然可以写 getx(&td)。此外,您可能不需要 getx,因为您可以编写 d.x(或 td.x 或 pd->x)。

最后使用 typeof gcc 扩展,您可以编写一个小宏来进行向下转换(即转换为更派生的结构)

#define TO(T,p) \
({ \
    typeof(p) nil = (T*)0; \
    (T*)((char*)p - ((char*)nil - (char*)0)); \
}) \

所以你可以做类似的事情

struct Base b = {0}, *pb = &b;
struct Derived* pd = TO(struct Derived, pb); 

如果您尝试使用函数指针执行虚函数,这很有用。

在 gcc 上,您可以通过 -fplan9-extensions 使用/试验 plan 9 扩展。不幸的是,它似乎没有在 clang 上实现。

【讨论】:

    猜你喜欢
    • 2011-12-06
    • 1970-01-01
    • 1970-01-01
    • 2021-05-21
    • 2014-08-27
    • 2015-02-06
    • 2011-01-14
    • 1970-01-01
    • 2012-11-24
    相关资源
    最近更新 更多