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

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

但是,我们有点担心别名问题,因为我们还不完全了解何时出现问题。

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

#include  /* -------- 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岁。

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

我们非常确定从Cat and Dog到IHello的’upcast’是安全的,因为IHello是这两种结构的第一个成员。

我们真正关心的是在SayHello的相应接口实现中分别从IHello到Cat and Dog的“向下倾斜”。 这会导致任何严格的别名问题吗? 我们的代码是否保证符合C标准,或者我们很幸运,这与gcc一起使用?

更新

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

这种’模式’的意图是客户端代码应该接收指向IHello的指针,因此只能调用接口中的函数。 但是,这些调用必须具有不同的行为,具体取决于收到的IHello的实现。 简而言之,我们希望实现此接口的接口和类的OOP概念具有相同的行为。

我们知道代码仅在IHello接口结构被放置为实现接口的结构的第一个成员时才有效。 这是我们愿意接受的限制。

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

§6.7.2.1/ 13:

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

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

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

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

根据上面的第五个子弹以及结构顶部不包含填充的事实,我们相当确定’upcasting’实现接口指向接口的接口的派生结构是安全的,即

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

并评论出’downcast’也编译并运行正常。 但是,这会为函数签名不匹配生成编译器警告。

多年来我一直在做c中的对象,完成你在这里做的那种组合。 我建议你不要做你正在描述的简单演员,但要certificate我需要一个例子。 例如,与分层实现一起使用的计时器回调机制:

 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 *我想,但我不是真正的标准律师)。

从您引用的标准部分:

指向适当转换的结构对象的指针指向其初始成员(或者如果该成员是位字段,则指向它所在的单元),反之亦然

将像cat-> hello这样的指针转换为Cat指针肯定是安全的,对于dog-> hello也是如此,所以SayHello函数中的强制转换应该没问题。

在调用站点,您正在做相反的事情:将指向结构的指针转换为指向第一个元素的指针。 这也保证有效。