My colleague and I are trying to achieve a simple polymorphic class hierarchy. We are working on an embedded system and are limited only to using the C compiler. We have a basic design idea that compiles without warning (-Wall -Wextra -fstrict-aliasing -pedantic) and works fine under gcc 4.8.1.
However, we are a little concerned about smoothing problems, as we do not fully understand when this becomes a problem.
To demonstrate, we wrote a toy example with the "interface" IHello and two classes that implement this interface "Cat" and "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; }
Output:
Good afternoon! I'm a cat! My name is Gauntlets, I'm 5 years old.
Hello! I am a dog! I can make this sound: Woof! I am 4 years old and weigh 10.3 kg.
We are confident that the take-off from Cat and Dog to IHello is safe, as IHello is the first member of both of these structures.
Our real problem is the downgrade from IHello to Cat and Dog, respectively, in the corresponding implementations of the SayHello interface. Does this mean any severe problems with the alias? Is our code guaranteed to work according to the C standard, or are we just lucky that this works with gcc?
Update
The solution that we ultimately decided to use should be standard C and cannot be supported, for example. gcc. The code should be able to compile and run on different processors using different (proprietary) compilers.
The purpose of this "template" is that client code must receive pointers to IHello and thus be able to call functions in the interface. However, these calls should behave differently depending on which IHello implementation was received. In short, we want identical behavior with the concept of interfaces and OOP classes that implement this interface.
We know that code only works if the IHello interface structure is placed as the first member of the structures implementing the interface. This is a limitation that we are ready to accept.
According to: Does accessing the first field of a structure using C cast violate a strict alias?
ยง6.7.2.1 / 13:
Inside the structure object, the members of the non-bit field and the units in which the bit fields are located have addresses that increase in the order in which they are declared. A pointer to a structure object, appropriately transformed, points to its initial member (or if this element is a bit field, and then to the block in which it is located) and vice versa. There may be an unnamed addition to the structure object, but not at the beginning.
The rule of aliases states the following (ยง6.5 / 7):
The object must have a stored value, accessed only by the lvalue expression, which has one of the following types:
- a type compatible with an efficient object type,
- qualified version of the type compatible with the effective type of the object,
- a type that is a signed or unsigned type corresponding to the effective type of the object,
- a type that is a signed or unsigned type corresponding to a qualified version of an effective object type,
- aggregate or type of association that includes one of the above types among its members (including recursively, a member of a sub-aggregate or contained association) or
- character type.
In accordance with the fifth mark above and the fact that there is no top-laying in the structures, we are pretty sure that โraising the levelโ of the derived structure that implements the interface for the interface pointer is safe, i.e.
Cat cat; const IHello* catPtr = (const IHello*) &cat; void Greet(const IHello* interface, const char* greeting) { interface->SayHello(interface, greeting); }
The big question is whether the "downcast" used when implementing interface functions is safe. As seen above:
void Cat_SayHello(const IHello* hello, const char* greeting) { const Cat* cat = (const Cat*) hello; }
Also note that changing the signature of the implementation functions to
Cat_SayHello(const Cat* cat, const char* greeting); Dog_SayHello(const Dog* dog, const char* greeting);
and commenting downcast also compiles and works fine. However, this generates a compiler warning for the mismatch of the function signature.