有没有办法让GCC / Clang知道C中的继承?

时间:2014-01-17 10:29:02

标签: c inheritance gcc clang

我正在编写一个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并在这里铿锵作响,因此欢迎编译器特定的答案。我有一些模糊的记忆,看到一些代码使用__attribute__((inherits(Base))或类似的东西来做这件事,但我的记忆可能在撒谎。

3 个答案:

答案 0 :(得分:5)

这在C中是安全的,除非你应该将参数强制转换为Base *。禁止别名的规则(或更确切地说,不包括在标准C中支持的规则)在C 2011 6.5中,其中第7段规定:

  

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

     

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

     

...

此规则阻止我们指向float,将其转换为指向int的指针,并取消引用指向int的指针以float作为{ {1}}。 (更准确地说,它并不妨碍我们尝试,但它会使行为不明确。)

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

  

...指向适当转换的结构对象的指针指向其初始成员...

因此,当我们将指向Base的指针转换为指向Derived的指针时,我们实际拥有的不是指向使用与其不同类型的Base对象的指针(这是一个指向Derived对象的第一个成员的指针,使用它的实际类型Derived,这是完全正常的。

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

答案 1 :(得分:1)

提供基本成员的地址(真正的类型安全选项):

getx(&d.base);

或者使用void指针:

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

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

它有效,因为C要求在第一个struct成员之前永远不会有填充。这不会增加类型安全性,但会消除对铸造的需求。

答案 2 :(得分:1)

如上所述,用户694733,您可能最好通过使用基本字段的地址来符合标准和类型安全性(重复以供将来参考)

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);

现在尽管基地不是你仍然可以做的第一个领域

getx (&d.b); 

或者如果你正在处理指针

getx(&pd->b). 

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

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

so&((Derived *)NULL) - > b变为

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

IMO错失的机会是C采用匿名结构但没有采用计划9匿名结构模型

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上,您可以使用/试验计划9扩展名-fplan9-extensions。不幸的是,它似乎没有在clang上实现。