Jon*_*anE 5 c oop polymorphism strict-aliasing
我和一位同事正试图实现一个简单的多态类层次结构.我们正在研究嵌入式系统,仅限于使用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;
}
Run Code Online (Sandbox Code Playgroud)
输出:
美好的一天!我是一只猫!我叫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接口结构被放置为实现接口的结构的第一个成员时才有效.这是我们愿意接受的限制.
§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 */
}
Run Code Online (Sandbox Code Playgroud)
最大的问题是,在实现接口功能时使用的"向下转换"是否安全.如上所示:
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 */
}
Run Code Online (Sandbox Code Playgroud)
另请注意,将实现函数的签名更改为
Cat_SayHello(const Cat* cat, const char* greeting);
Dog_SayHello(const Dog* dog, const char* greeting);
Run Code Online (Sandbox Code Playgroud)
并评论出'downcast'也编译并运行良好.但是,这会生成函数签名不匹配的编译器警告.
多年来我一直在用 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;
};
Run Code Online (Sandbox Code Playgroud)
当这些计时器之一到期时,管理系统调用 m_expiry 函数并将指针传递给对象:
timer->m_expiry(timer);
Run Code Online (Sandbox Code Playgroud)
然后使用一个基础对象来完成一些令人惊奇的事情:
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");
}
Run Code Online (Sandbox Code Playgroud)
我的命名系统在这里有一个目的,但这并不是真正的重点。我们可以看到可能需要的各种面向对象的函数调用:
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);
}
Run Code Online (Sandbox Code Playgroud)
自 1990 年左右以来,我一直在为 GetObjectFromMember 使用相同的宏,并且 Linux 内核在某个地方创建了相同的宏并将其称为“container_of”(尽管参数的顺序不同):
#define GetObjectFromMember(ObjectType,MemberName,MemberPointer) \
((ObjectType *)(((char *)MemberPointer) - ((char *)(&(((ObjectType *)0)->MemberName)))))
Run Code Online (Sandbox Code Playgroud)
它依赖于(技术上)未定义的行为(取消引用 NULL 对象),但可以移植到我测试过的每个旧(和新)c 编译器。新版本需要 offsetof 宏,它现在是标准的一部分(显然从 C89 开始):
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type,member) );})
Run Code Online (Sandbox Code Playgroud)
当然,我更喜欢我的名字,但无论如何。使用此方法使您的代码不依赖于将基础对象放在第一位,并且还使第二个用例成为可能,我发现这在实践中非常有用。所有别名编译器问题都在宏内管理(我认为是通过 char * 进行转换,但我并不是真正的标准律师)。
| 归档时间: |
|
| 查看次数: |
537 次 |
| 最近记录: |