c - C 中的 OO 多态性,别名问题?

标签 c oop polymorphism strict-aliasing

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

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

为了演示,我们编写了一个玩具示例,其中包含一个“接口(interface)”IHello 和两个实现此接口(interface)“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 公斤。

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

我们真正关心的是在 SayHello 的相应接口(interface)实现中分别从 IHello 向下转换为 Cat 和 Dog。这会导致任何严格的别名问题吗?我们的代码是否保证按照 C 标准工作,或者我们只是幸运地与 gcc 一起工作?

更新

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

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

我们知道只有当 IHello 接口(interface)结构被放置为实现该接口(interface)的结构的第一个成员时,代码才有效。这是我们愿意接受的限制。

根据:Does accessing the first field of a struct via a C cast violate strict aliasing?

§6.7.2.1/13:

Within a structure object, the non-bit-field members and the units in which bit-fields reside have addresses that increase in the order in which they are declared. A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.

The aliasing rule reads as follows (§6.5/7):

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
  • a character type.

根据上面的第五点和结构顶部不包含填充的事实,我们相当确定将实现接口(interface)的派生结构“向上转换”为指向接口(interface)的指针是安全的,即

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 */
}

最大的问题是在接口(interface)函数的实现中使用的“向下转换”是否安全。 如上所示:

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 语言编写对象,所做的正是您在这里所做的那种组合。我将建议您不要执行您所描述的简单转换,但为了证明我需要一个示例。例如,与分层实现一起使用的计时器回调机制:

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 * 进行转换,但我并不是真正的标准律师)。

关于c - C 中的 OO 多态性,别名问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31477307/

相关文章:

c - CodeBlocks 中的 Free 函数非常慢

c - 如何将字符串数组从 fortran 传递给 c?

c# - 如果重写的方法返回不同的值,是否违反了里氏原则?

python - 如何创建从外部库中的 sqlalchemy 模型继承的自定义类

c# - Controller 中的 MVC5 不明确操作方法

c++ - 使用 `dynamic_cast` 来推断在基类上定义并在派生类上实现的成员函数的参数类型是否正确?

c - 高效的文件系统搜索

c - 为什么 "code"与 "name"相同?

c++ - 在 mac os x (10.8) 中链接

c++ - 在哪里编写类的实现