C中的OO多态性,混叠问题?

乔纳丹

我和一个同事正在尝试实现简单的多态类层次结构。我们正在嵌入式系统上工作,并且仅限于使用C编译器。我们有一个基本的设计思想,可以在没有警告的情况下进行编译(-Wall -Wextra -fstrict-aliasing -pedantic),并且可以在gcc 4.8.1下正常运行。

但是,我们对混叠问题有些担心,因为当出现问题时我们还不完全了解。

为了演示,我们编写了一个带有“接口” IHello的玩具示例,以及两个实现此接口“ Cat”和“ Dog”的类。

#include <stdio.h>

/* -------- IHello -------- */
struct IHello_;
typedef struct IHello_
{
    void (*SayHello)(const struct IHello_* self, const char* greeting);
} IHello;

/* Helper function */
void SayHello(const IHello* self, const char* greeting)
{
    self->SayHello(self, greeting);
}

/* -------- Cat -------- */
typedef struct Cat_
{
    IHello hello;
    const char* name;
    int age;
} Cat;

void Cat_SayHello(const IHello* self, const char* greeting)
{
    const Cat* cat = (const Cat*) self;
    printf("%s I am a cat! My name is %s and I am %d years old.\n",
           greeting,
           cat->name,
           cat->age);
}

Cat Cat_Create(const char* name, const int age)
{
    static const IHello catHello = { Cat_SayHello };
    Cat cat;

    cat.hello = catHello;
    cat.name = name;
    cat.age = age;

    return cat;
}

/* -------- Dog -------- */
typedef struct Dog_
{
    IHello hello;
    double weight;
    int age;
    const char* sound;
} Dog;

void Dog_SayHello(const IHello* self, const char* greeting)
{
    const Dog* dog = (const Dog*) self;
    printf("%s I am a dog! I can make this sound: %s I am %d years old and weigh %.1f kg.\n",
           greeting,
           dog->sound,
           dog->age,
           dog->weight);
}

Dog Dog_Create(const char* sound, const int age, const double weight)
{
    static const IHello dogHello = { Dog_SayHello };
    Dog dog;

    dog.hello = dogHello;
    dog.sound = sound;
    dog.age = age;
    dog.weight = weight;

    return dog;
}

/* Client code */
int main(void)
{
    const Cat cat = Cat_Create("Mittens", 5);
    const Dog dog = Dog_Create("Woof!", 4, 10.3);

    SayHello((IHello*) &cat, "Good day!");
    SayHello((IHello*) &dog, "Hi there!");

    return 0;
}

输出:

美好的一天!我是一只猫!我叫Mittens,今年5岁。

嗨,您好!我是一只狗!我可以发出这样的声音:哇!我今年4岁,体重10.3公斤。

我们非常确定,从猫和狗到IHello的“转换”是安全的,因为IHello是这两种结构的第一个成员。

我们真正关心的是在SayHello的相应接口实现中分别从IHello到Cat和Dog的“贬低”。这会引起任何严格的混叠问题吗?我们的代码是否可以保证按C标准运行,还是很幸运能与gcc一起使用?

更新资料

我们最终决定使用的解决方案必须是标准C,并且不能依赖于gcc扩展。该代码必须能够使用各种(专有)编译器进行编译并在不同的处理器上运行。

这种“模式”的意图是客户代码应接收指向IHello的指针,因此只能在接口中调用函数。但是,根据收到的IHello的实现,这些调用的行为必须不同。简而言之,我们希望行为与接口和实现此接口的类的OOP概念相同。

我们知道以下事实:仅当将IHello接口结构放置为实现该接口的结构的第一个成员时,该代码才有效。这是我们愿意接受的限制。

根据:通过C强制转换访问结构的第一个字段是否违反严格的别名?

§6.7.2.1/ 13:

在结构对象中,非位字段成员和位字段所在的单元的地址按照声明的顺序增加。指向经过适当转换的结构对象的指针指向其初始成员(或者,如果该成员是位字段,则指向它所驻留的单元),反之亦然。结构对象内可能存在未命名的填充,但在其开始处没有。

别名规则如下(第6.5 / 7节):

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

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 类型是与对象的有效类型相对应的有符号或无符号类型,
  • 一种类型,是与对象的有效类型的限定版本相对应的有符号或无符号类型,
  • 在其成员(包括递归地,子集合或包含的联盟的成员)中包括上述类型之一的集合或联合类型,或
  • 字符类型。

根据上面的第五个项目符号,以及结构的顶部不包含填充的事实,我们可以确定,“向上转换”实现了指向接口指针的接口的派生结构是安全的,即

Cat cat;
const IHello* catPtr = (const IHello*) &cat; /* Upcast */

/* Inside client code */
void Greet(const IHello* interface, const char* greeting)
{
    /* Users do not need to know whether interface points to a Cat or Dog. */
    interface->SayHello(interface, greeting); /* Dereferencing should be safe */
}

最大的问题是在实现接口功能时使用的“向下转换”是否安全。如上所示:

void Cat_SayHello(const IHello* hello, const char* greeting)
{
    /* Is the following statement safe if we know for
     * a fact that hello points to a Cat?
     * Does it violate strict aliasing rules? */
    const Cat* cat = (const Cat*) hello;
    /* Access internal state in Cat */
}

另请注意,将实现功能的签名更改为

Cat_SayHello(const Cat* cat, const char* greeting);
Dog_SayHello(const Dog* dog, const char* greeting);

并注释掉“向下转换”也可以编译并正常运行。但是,这会生成针对功能签名不匹配的编译器警告。

速8

我从事c语言的对象已经很多年了,完全按照您在此处所做的构图进行。我建议您不要做您正在描述的简单转换,而是要证明我需要一个例子。例如,用于分层实现的计时器回调机制:

typedef struct MSecTimer_struct MSecTimer;
struct MSecTimer_struct {
     DoubleLinkedListNode       m_list;
     void                       (*m_expiry)(MSecTimer *);
     unsigned int               m_ticks;
     unsigned int               m_needsClear: 1;
     unsigned int               m_user: 7;
};

当这些计时器之一到期时,管理系统将调用m_expiry函数并将指针传递给该对象:

timer->m_expiry(timer);

然后拿一个做一些令人惊奇的基础对象:

typedef struct BaseDoer_struct BaseDoer;
struct BaseDoer_struct
{
     DebugID      m_id;
     void         (*v_beAmazing)(BaseDoer *);  //object's "virtual" function
};

//BaseDoer's version of BaseDoer's 'virtual' beAmazing function
void BaseDoer_v_BaseDoer_beAmazing( BaseDoer *self )
{
    printf("Basically, I'm amazing\n");
}

我的命名系统在这里有目的,但这并不是重点。我们可以看到可能需要进行多种面向对象的函数调用:

typedef struct DelayDoer_struct DelayDoer;
struct DelayDoer_struct {
     BaseDoer     m_baseDoer;
     MSecTimer    m_delayTimer;
};

//DelayDoer's version of BaseDoer's 'virtual' beAmazing function
void DelayDoer_v_BaseDoer_beAmazing( BaseDoer *base_self )
{
     //instead of just casting, have the compiler do something smarter
     DelayDoer *self = GetObjectFromMember(DelayDoer,m_baseDoer,base_self);

     MSecTimer_start(m_delayTimer,1000);  //make them wait for it
}

//DelayDoer::DelayTimer's version of MSecTimer's 'virtual' expiry function
void DelayDoer_DelayTimer_v_MSecTimer_expiry( MSecTimer *timer_self )
{
    DelayDoer *self = GetObjectFromMember(DelayDoer,m_delayTimer,timer_self);
    BaseDoer_v_BaseDoer_beAmazing(&self->m_baseDoer);
}

自1990年左右以来,我一直在为GetObjectFromMember使用相同的宏,并且沿着这行代码的某个地方,Linux内核创建了相同的宏,并将其称为container_of(尽管参数的顺序不同):

  #define GetObjectFromMember(ObjectType,MemberName,MemberPointer) \
              ((ObjectType *)(((char *)MemberPointer) - ((char *)(&(((ObjectType *)0)->MemberName)))))

它依赖于(技术上)未定义的行为(取消引用NULL对象),但是可移植到我测试过的每个旧(新)c编译器中。较新的版本需要offsetof宏,该宏现在已成为标准的一部分(显然自C89起):

#define container_of(ptr, type, member) ({ \
            const typeof( ((type *)0)->member ) *__mptr = (ptr); 
            (type *)( (char *)__mptr - offsetof(type,member) );})

我当然更喜欢我的名字,但是随便什么。使用此方法可以使您的代码不依赖于将基础对象放在首位,也可以使第二个用例成为可能,在实践中我认为这非常有用。所有混叠编译器问题都在宏中管理(通过char *广播,我想,但我并不是真正的标准律师)。

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章