2015-07-17 12 views
5

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.

+1

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

+0

Und dann wäre es einfacher, keinen Zeiger an erster Stelle zu haben: p –

+0

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

Antwort

1

Ich mache seit vielen Jahren Objekte in c und mache genau die Art von Komposition, die Sie hier machen. Ich werde Ihnen empfehlen nicht tun die einfache Besetzung, die Sie beschreiben, aber zu rechtfertigen, dass ich ein Beispiel brauche.

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

Wenn einer dieser Timer abläuft, das Verwaltungssystem ruft die m_expiry Funktion und gelangt in den Zeiger auf das Objekt:: beispielsweise mit einem geschichteten Implementierung ein Timer Rückrufmechanismus verwendet

timer->m_expiry(timer); 

Dann nehmen ein Basisobjekt, das etwas Erstaunliches bewirkt:

Mein Benennungssystem hat einen Zweck hier, aber das ist nicht wirklich der Fokus.Wir können eine Vielzahl von objektorientierten Funktionsaufrufe sehen, die erforderlich sein könnten:

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

Ich habe seit etwa 1990 die gleiche Makro für GetObjectFromMember wurde verwendet und irgendwo entlang der Strecke der Linux-Kernel erstellt das gleiche Makro und rief es container_of (die Parameter sind in einer anderen Reihenfolge obwohl):

#define GetObjectFromMember(ObjectType,MemberName,MemberPointer) \ 
       ((ObjectType *)(((char *)MemberPointer) - ((char *)(&(((ObjectType *)0)->MemberName))))) 

die (dereferencing ein NULL-Objekt) auf (technisch) nicht definiertes Verhalten beruht, ist aber tragbar jeden alten (und neuer) c-Compiler ich habe jemals getestet. Die neuere Version erfordert die offsetof Makro, das nun Teil der Norm ist (Stand C89 scheinbar):

#define container_of(ptr, type, member) ({ \ 
      const typeof(((type *)0)->member) *__mptr = (ptr); 
      (type *)((char *)__mptr - offsetof(type,member));}) 

ich natürlich lieber meinen Namen, aber was auch immer. Wenn Sie diese Methode verwenden, ist Ihr Code nicht darauf angewiesen, das Basisobjekt an erster Stelle zu setzen, und auch der zweite Anwendungsfall wird möglich, was ich in der Praxis als sehr nützlich empfinde. Alle Alias-Compiler-Probleme werden innerhalb des Makros verwaltet (Casting durch den Char * denke ich, aber ich bin nicht wirklich ein Standard-Anwalt).

+0

Danke für deine ausführliche Antwort! Derzeit verwenden wir ein Makro, das Ihrem GetObjectFromMember ähnlich ist. Wie Sie erläutert haben, können wir das Basisobjekt an einer beliebigen Stelle in der Struktur platzieren und können auch die Mehrfachvererbung verwenden. – JonatanE

2

Aus dem Abschnitt der Standard, den Sie zitiert:

Ein Zeiger auf eine Struktur-Objekt, in geeigneter Weise umgewandelt, weist auf seine Gründungsmitglied (oder wenn das Element ein Bit-Feld, dann die Einheit in , die sie sich befindet) und

vice versa

es ist auf jeden Fall sicher einen Zeiger wie Katzen-> hallo in einen Cat-Zeiger, und in ähnlicher Weise für Hund-> hallo, so die Modelle in Ihrer SayHello Funktionen umwandeln sollte gut.

Bei der Aufrufstelle machen Sie das Gegenteil: einen Zeiger in eine Struktur in einen Zeiger auf das erste Element konvertieren. Das funktioniert auch garantiert.