Ich und ein Kollege versuchen, eine einfache polymorphe Klassenhierarchie zu erreichen. Wir arbeiten an einem eingebetteten System und beschränken uns darauf, nur einen C-Compiler zu verwenden. Wir haben eine grundlegende Designidee, die ohne Warnungen kompiliert (-Wall -Wextra -fstrict-aliasing -pedantic) und läuft gut unter gcc 4.8.1.OO Polymorphismus in C, Aliasing-Probleme?
Allerdings sind wir ein bisschen besorgt über Aliasing-Probleme, da wir nicht vollständig verstehen, wenn dies ein Problem wird.
Um zu demonstrieren, haben wir ein Spielzeugbeispiel mit einer 'Schnittstelle' IHello und zwei Klassen geschrieben, die diese Schnittstelle 'Katze' und 'Hund' implementieren.
#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;
}
Ausgang:
Guten Tag! Ich bin eine Katze! Mein Name ist Fäustlinge und ich bin 5 Jahre alt.
Hallo zusammen! Ich bin ein Hund! Ich kann diesen Ton machen: Wuff! Ich bin 4 Jahre alt und wiege 10,3 kg.
Wir sind ziemlich sicher, dass die 'Upcast' von Cat und Dog zu IHello sicher ist, da IHallo das erste Mitglied dieser beiden Strukturen ist.
Unser eigentliches Anliegen ist der 'downcast' von IHallo zu Cat und Dog bzw. zu den entsprechenden Schnittstellenimplementierungen von SayHello. Führt dies zu strengen Aliasing-Problemen? Ist unser Code garantiert mit dem C-Standard kompatibel oder haben wir einfach Glück, dass dies mit gcc funktioniert?
aktualisieren
Die Lösung, die wir verwenden entscheiden schließlich Standard C liegen und verlassen sich nicht auf z.B. gcc-Erweiterungen. Der Code muss kompilieren und auf verschiedenen Prozessoren mit verschiedenen (proprietären) Compilern ausgeführt werden können.
Die Absicht mit diesem 'Muster' ist, dass der Client-Code Zeiger auf IHello erhalten soll und somit nur Funktionen in der Schnittstelle aufrufen kann. Diese Aufrufe müssen sich jedoch unterschiedlich verhalten, je nachdem, welche Implementierung von IHallo empfangen wurde. Kurz gesagt, wir wollen das gleiche Verhalten wie das OOP-Konzept von Schnittstellen und Klassen, die diese Schnittstelle implementieren.
Wir sind uns der Tatsache bewusst, dass der Code nur funktioniert, wenn die IHello-Schnittstellenstruktur als erstes Mitglied der Strukturen platziert wird, die die Schnittstelle implementieren. Dies ist eine Einschränkung, die wir akzeptieren wollen.
Nach: Does accessing the first field of a struct via a C cast violate strict aliasing?
§6.7.2.1/13:
Innerhalb eines Strukturobjekts, die nicht-Bitfeld Elemente und die Einheiten, in denen Bitfelder Adressen befinden, erhöhen in der Reihenfolge, in der sie erklärt werden. Ein Zeiger auf ein Strukturobjekt, das geeignet konvertiert wurde, zeigt auf sein Anfangselement (oder wenn dieses Element ein Bitfeld ist, dann auf die Einheit, in der es sich befindet) und umgekehrt. Es kann ein unbenanntes Padding innerhalb eines Strukturobjekts geben, aber nicht an seinem Anfang.
Die Alias-Regel lautet wie folgt (§6.5/7):
Ein Objekt hat seinen gespeicherten Wert nur durch einen Ausdruck L-Wert zugegriffen, die einen der folgenden Typen hat:
- ein Typ kompatibel mit dem wirksamen Typ des Objekts,
- eine qualifizierte Version eines Typs, der mit dem effektiven Objekttyp kompatibel ist,
- ein Typ, bei dem es sich um den vorzeichenbehafteten oder vorzeichenlosen Typ handelt, der dem effektiven Objekttyp entspricht,
- ein Typ, der dem Typ mit oder ohne Vorzeichen entspricht eine qualifizierte Version der ef fictive type des Objekts,
- ein Aggregat- oder Union-Typ, der einen der oben genannten Typen unter seinen Mitgliedern enthält (einschließlich rekursiv eines Mitglieds eines Unteraggregats oder einer enthaltenen Union) oder
- ein Zeichentyp.
Gemäß der fünften Kugel oben und der Tatsache, dass Strukturen keine Polsterung an der Spitze enthalten wir ziemlich sicher sind, dass ‚Upcasting‘ eine abgeleiteter Struktur, die die Schnittstelle zu einem Zeiger auf die Schnittstelle implementiert ist sicher, dh
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 */
}
Die große Frage ist, ob die "Downcast" in der Implementierung der Schnittstelle Funktion (en) verwendet sicher ist. Wie oben gesehen:
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 */
}
Beachten Sie auch, dass die Unterzeichnung der Umsetzung Funktionen
Cat_SayHello(const Cat* cat, const char* greeting);
Dog_SayHello(const Dog* dog, const char* greeting);
und Kommentierung aus dem ‚gesenkten‘ erstellt auch und läuft Wechsel in Ordnung. Dies generiert jedoch eine Compiler-Warnung für die Funktionssignaturfehler.
Es wäre viel einfacher, wenn die Strukturen Katze und Hund einen eigenen Zeiger auf seine eigene Funktion hätten. Dann würden Sie einfach anrufen: 'cat.SayHello (& Katze," Guten Tag! ");' – this
Und dann wäre es einfacher, keinen Zeiger an erster Stelle zu haben: p –
Wenn wir polymorphes Verhalten nicht wollten, dann könnten wir verwende einfach zwei separate Funktionen, z 'void Cat_SayHello (Cat * cat)' und 'void Dog_SayHello (Dog * dog)' aber das ist nicht ausreichend. Die Idee hier ist, dass der Client-Code nur ein 'IHello *' empfängt und dann nur Funktionen aufrufen kann, die von dieser Schnittstelle implementiert werden, aber das Ergebnis wird davon abhängen, welche Implementierung von'Hallo' an den Client-Code übergeben wurde. – JonatanE