Alternative zu virtuellen Dummy-Funktionen in abstr. Basisklassen



  • Hallo,

    in einem Programm habe ich sog. Kanalklassen (alle von der abstr. Klasse ChannelA vererbt) und DeviceDriver-Klassen (alle von der abstr. Klasse DeviceDriverA vererbt).
    Jedes Kanalobjekt soll einen DeviceDriver als Pointer beinhalten. Dazu gibt's in der Klasse DeviceChnA einen Ptr auf die Klasse DeviceDriverA.

    Die versch. konkreten DeviceDriver haben verschiedene Methoden. Konkrete DeviceDriver-Objekte werden bei der Erstellung eines konkreten Kanals angelegt. Es sieht also so aus:

    class ChannelA
    {
    /* ... */
    protected:	
    	DeviceDriverA m_devicedrv;
    }
    
    class ConcrChn1 : public ChannelA
    {
    /* ... */
    }
    
    class ConcrDevice1 : public DeviceDriverA
    {
    	public:
    		void aSpecialMethod();
    /* ... */
    }
    
    ConcrChnA::ConcrChnA
    {
    /* ... */
    	m_devicedrv = new ConcrDevice1(param);
    }
    
    ConcrChnA::doSomething()
    {
    /* ... */
        m_devicesdrv->aSpecialMethod(param); /* hier meckert der Compiler */
    }
    

    Der Compiler (gcc 4.3.2 unter Linux) beschwert sich dann bei der Verwendung der Methode aSpecialMethod():

    error: 'class DeviceDriverA' has no member named 'aSpecialMethod'

    Offenbar ist es dem Compiler nicht genug, dass das new ein Objekt der Klasse ConcrDevice1 alloziert. Er stört sich daran dass m_devicedrv vom Typ DeviceDriverA ist. Ich habe dann bisher die Lösung verwendet, dass ich in der Klasse DeviceDriverA eine virtuelle Dummy-Funktion implementiere (die gibt nur eine Warnung aus dass - falls die Funktion tatsächlich aufgerufen wird - ein Fehler im Quellcode vorliegt). Das funktioniert zwar, ist aber sehr häßlich.

    Das kann man bestimmt schöner machen, aber wie?

    Dankbar für alle Tipps, Stephan



  • normalerweise brauchen die abstrakten Klassen kein spezielles Wissen über die implementierungsklassen. Sprich, die basis-Channelklasse braucht nicht zu wissen dass irgendeiner der DeviceDriver eine spezielle Methode hat. Je nachdem liegt folgendes vor: Je nach ChannelKlasse hast du einen eigenen DeviceDriver, dann hat der Pointer auf die DeviceDriver-Klasse nicht unbedingt was in der basis-Channelklasse verloren. Oder die spezielle Methode wird nur von dem einen DeviceDrivr benutzt, um eine allgemeine Operation (die jeder Driver hat) auf spezielle Art auszuführen. Dann gibts für die allgemeine operation eine virtuelle funktion, und deren überladung in dem einen driver ruft die spezielle methode auf.



  • pumuckl schrieb:

    normalerweise brauchen die abstrakten Klassen kein spezielles Wissen über die implementierungsklassen. Sprich, die basis-Channelklasse braucht nicht zu wissen dass irgendeiner der DeviceDriver eine spezielle Methode hat.

    Das habe ich auch gedacht, aber dann dürfte der Compiler den Fehler doch auf keinen Fall bringen...

    pumuckl schrieb:

    Je nachdem liegt folgendes vor: Je nach ChannelKlasse hast du einen eigenen DeviceDriver, dann hat der Pointer auf die DeviceDriver-Klasse nicht unbedingt was in der basis-Channelklasse verloren. Oder die spezielle Methode wird nur von dem einen DeviceDrivr benutzt, um eine allgemeine Operation (die jeder Driver hat) auf spezielle Art auszuführen. Dann gibts für die allgemeine operation eine virtuelle funktion, und deren überladung in dem einen driver ruft die spezielle methode auf.

    Teils teils...

    Es sind ca. 10 Channel-Klassen und 3 DeviceDriver-Klassen. Jeder konkr. DeviceDriver findet in mehreren Channel-Klassen Verwendung.

    Vom Aufbau der Klassen her finde ich es sehr logisch dass ich eine abstrakte DeviceDriver-Klasse in der abstr. Kanal-Klasse einbinde, denn einige Funktionen kann ich als rein-virtuell in der DeviceDriver-Basisklasse deklarieren.

    Andere Funktionen werden - siehe Beispiel - erst in den konkreten DeviceDriver-Klassen deklariert.

    Es wird wohl auch mal Kanäle geben, die erst zur Laufzeit (durch Benutzerkonfiguration) erfahren welchen konkreten DeviceDriver sie verwenden sollen. Auch dazu ist die Einbindung der abstr. DeviceDriver-Klasse von Vorteil (sonst müsste ich ja in einer solchen Kanal-Klasse alle möglichen konkr. DeviceDriver-Klassen als Membervar. deklarieren, würde aber letztendlich nur eine dieser Variablen verwenden).

    Ich habe jetzt noch was probiert:

    ConcrChnA::doSomething()
    {
        /* ... */
        // m_devicesdrv->aSpecialMethod(param); /* hier meckert der Compiler */
        ConcrDevice1* temp = new ConcrDevice1;
        temp = static_cast<ConcrDevice1*>(m_sensordev);
        temp->aSpecialMethod(param); /* jetzt geht's */
    }
    

    So geht's, aber will das man natürlich nicht bei jedem Funktionsaufruf machen müssen...

    edit: Grammatik-Fehler verbessert.



  • Doch gerade darum bringt der Compiler einen Fehler, weil man über den Basisklassenzeiger nur Methoden, welche in der Basisklasse definiert sind, aufrufen kann.

    Also entweder extra casten, z.B. mittels einer privaten Methode:

    ConcrDevice1* ConcrChnA::GetConcrDevice1() const
    {
      return static_cast<ConcrDevice1>(m_devicedrv);
    }
    
    // Aufruf innerhalb von ConcrChnA:
    GetConcrDevice1()->aSpecialMethod(param);
    

    Und die andere (m.E. bessere) Alternative wäre mittels Templates, d.h. die Übergabe des konkreten DeviceDriver an die Channel-Basisklasse.

    P.S: dein Konvertierungscode ist zu umständlich (die Erzeugung mittels new ist völlig unnötig: wird sofort wieder überschrieben -> Speicherleck!!!)



  • Die Frage ist, wer den Devicedriver eigentlich verwaltet - wenn die abgeleitete Klasse tatsächlich Zugriff auf den konkreten Typ des Drivers haben muss, dann istd er Pointer auf den devicedriver in der abgeleiteten Klasse anzulegen. Und zwar mit dem konkreten Typ, sonst kann die abgeleitete Klasse auch nicht auf die spezifischen Methoden zugreifen. Wenn die Channel-Basisklasse trotzdem Zugriff auf das allgemeine Interface des Drivers braucht, kann sie eine pure virtuelle Funktion GetDeviceDriver() deklarieren, über die sie das Ding dann bekommt:

    struct DDBase
    {
      virtual void driveIt() = 0;
    };
    
    struct ChannelBase
    {
      virtual DDBase* getDriver() = 0;  
      void channelit() { getDriver()->driveit(); }
    };
    
    struct SpecialDD : public DDBase
    {
      virtual void driveIt() { cout << "brrrrm...!" << endl; }
      void somethingSpecial() { cout << "need fuel!" << endl; }
    };
    
    class ChannelA
    {
      SpecialDD* myDD;
    public:
      ChannelA() : myDD(new SpecialDD) {}
      virtual DDBase* getDriver() { return myDD; }
      void channelTheSpecialA() { myDD->somethingspecial(); }
    };
    


  • Th69 schrieb:

    Doch gerade darum bringt der Compiler einen Fehler, weil man über den Basisklassenzeiger nur Methoden, welche in der Basisklasse definiert sind, aufrufen kann.

    Schade, ich dachte dass für den Typ in meinem Fall zählt welche Klasse ich für die Erzeugung mit new angebe. Naja, wieder was dazugelernt.

    Th69 schrieb:

    Und die andere (m.E. bessere) Alternative wäre mittels Templates, d.h. die Übergabe des konkreten DeviceDriver an die Channel-Basisklasse.

    Äh...?? D.h. einen Template-Typen als Member-Variable verwenden? Allerdings möchte ich ja erst im Channel entscheiden welchen DeviceDriver ich verwenden will. Die Erstellung des DeviceDriver gehört für mich in den Channel rein.

    Naja...ich schätze ich werde mal die erstere Variante umsetzen. Es ist auf jeden Fall sauberer als meine bisherige Lösung.

    Th69 schrieb:

    P.S: dein Konvertierungscode ist zu umständlich (die Erzeugung mittels new ist völlig unnötig: wird sofort wieder überschrieben -> Speicherleck!!!)

    Stimmt natürlich. Ich hatte das nicht im Einsatz sondern nur zum Testen ob sich der Compiler dann noch beschwert.

    Danke sehr!



  • Radix schrieb:

    Th69 schrieb:

    Doch gerade darum bringt der Compiler einen Fehler, weil man über den Basisklassenzeiger nur Methoden, welche in der Basisklasse definiert sind, aufrufen kann.

    Schade, ich dachte dass für den Typ in meinem Fall zählt welche Klasse ich für die Erzeugung mit new angebe. Naja, wieder was dazugelernt.

    Das ist der Unterschied zwischen statischem und dynamischem Typ. Funktionsaufrufe müssen zum statischen Typ passen.



  • pumuckl schrieb:

    Die Frage ist, wer den Devicedriver eigentlich verwaltet - wenn die abgeleitete Klasse tatsächlich Zugriff auf den konkreten Typ des Drivers haben muss, dann istd er Pointer auf den devicedriver in der abgeleiteten Klasse anzulegen. Und zwar mit dem konkreten Typ, sonst kann die abgeleitete Klasse auch nicht auf die spezifischen Methoden zugreifen.

    Dabei hab ich halt nach wie vor das Problem, dass manche Kanälen erst zur Laufzeit entscheiden sollen, welchen DeviceDriver sie benötigen.

    Ich dachte erst dass das ganze leichter wäre. Die Konvertierungsfunktion scheint auf den ersten Blick die simpelste Variante zu sein, ist aber im Prinzip auch nicht viel anders als den konkreten DeviceDriver in jede Kanal-Klasse einzubinden. Das Problem mit der Entscheidung zur Laufzeit bleibt dabei bestehen - ich bräuchte dazu wohl mehrere Konvertierungsfunktionen (die sich bzgl des Typs des Rückgabewerts unterscheiden) und vor dem Aufruf einer Konvertierungsfunktion eine Fallunterscheidung.

    Was imho das Problem lösen könnte, wäre eine Konvertierungs-Funktion mit einer Fallunterscheidung und einem Template-Rückgabeparameter. Für die Fallunterscheidung würde ich den Typ des DeviceDrivers dann in einer Membervariablen eines enum-Typ speichern.

    enum DevDrvType { ConcrDevDrv1 = 0, ConcrDevDrv2, ConcrDevDrv3 };
    
    class ConcrChn1
    {
        protected:
            enum DevDrvType m_devtype; // wird beim Erstellen des DeviceDriver mit dem passenden Wert belegt
    }
    
    template <class T>
    T* getConcreteDevDrv() 
    {
        switch(m_devtype)
        {
          case ConcrDevDrv1 :
            return static_cast<ConcrDevDrv1*>(m_devicedrv);
            break;
          case ConcrDevDrv2 :
            return static_cast<ConcrDevDrv2*>(m_devicedrv);
            break;
          case ConcrDevDrv3 :
            return static_cast<ConcrDevDrv3*>(m_devicedrv);
            break;
          // default sollte hier normalerweise entfallen
        }
    }
    

    Wäre sowas eine ordentliche Lösung oder hab ich mich mit der Idee verhoben?



  • Mit Template-Klasse meinte ich folgendes:

    template<typename T>
    class ChannelA
    {
    /* ... */
    protected:   
        T *m_devicedrv;
    }
    
    class ConcrChn1 : public ChannelA<ConcrDevDrv1>
    {
    /* ... */
    }
    

    So hat 'm_devicedrv' gleich den richtigen Typ.

    Aber pumuckl's Lösung ist genau so zu empfehlen, d.h. mit Polymorphie.



  • Nachdem meine Kanalklassen bei der Erstellung (im Konstruktor) selbst dafür sorgen müssen, den richtigen DeviceDriver zu verwenden und der DeviceDriver im C'tor eines Kanals erstellt wird, werde ich doch mal meine oben vorgeschlagene Lösung verwenden.

    Die anderen vorgeschlagenen Lösungen (Template-Klasse/Polymorphie) würden nur funktionieren, wenn für jeden Kanal der Typ des DeviceDrivers feststehen würde.



  • In welcher Weise interagieren denn Channel und Driver Objekte? Kannst du das nicht über ein Nachrichtensystem implementieren, in dem die unterschiedlichen Driver Typen auf die entsprechenden Ereignisse reagieren (und sie ggf. ignorieren).
    Die Lösung über eine enum und static_casts halte ich für nicht gut, vielleicht bietet sich da das Visitor Pattern an. Bei polymoprhen downcasts solltest du sowieso dynamic_cast statt static_cast benutzen.



  • DocShoe schrieb:

    In welcher Weise interagieren denn Channel und Driver Objekte?

    Das Programm nimmt Messungen verschiedener Größen vor. Ein Kanal entspricht einer Messgröße (z.B. Temperatur) und wird mit einer einzeiligen Anweisung vom Benutzer in einer Konfig.Datei deklariert.

    Das Programm parst die Datei. Das erste "Token" in einer Kanal-Zeile wird direkt ausgewertet und entspricht einem Kanaltyp. Der Inhalt der restlichen Zeile wird in eine Map<string, string> eingelesen.

    Entsprechend dem ersten Token wird ein Kanal vom passenden Typ erstellt (new). Dem c'tor des Kanals wird dabei die Map übergeben. Im c'tor wird die Map ausgewertet und festgestellt welches Messgerät (-> DeviceDriver) verwendet werden soll. Der passende DeviceDriver wird mit new erstellt, dem c'tor des DeviceDriver wird ebenfalls die Map übergeben, so dass er Einstellungen vornehmen kann. Die Art des Messgeräts kann nicht unbedingt anhand des Kanaltyps festgestellt werden, z.B. für Temperatur können versch. Messgeräte verwendet werden.

    (Mir wurde auch geraten eine Factory Method für die Konfiguration der Kanäle zu verwenden anstatt das in den c'tors zu machen. Ich hab dieses Muster nur nicht gleich verstanden, außerdem war ich da auch schon zu weit mit dem Programm fortgeschritten.)

    Die DeviceDriver übernehmen praktisch die Kommunikation mit den Messgeräten.
    Messungen werden im Kanal mit einem Aufruf einer DeviceDriver-Methode gestartet. Für ausgefallene Messgeräte gibt es DeviceDriver-Methoden für die Fehlerbehebung (reset, erneute Konfiguration). Die Messgeräte können sehr verschieden sein. Es gibt welche die über Netzwerk angeschlossen sind, andere sind über RS232 angeschlossen.

    Der DeviceDriver greift also nicht auf den Kanal zu, nur der Kanal benutzt den DeviceDriver.

    DocShoe schrieb:

    Kannst du das nicht über ein Nachrichtensystem implementieren, in dem die unterschiedlichen Driver Typen auf die entsprechenden Ereignisse reagieren (und sie ggf. ignorieren).
    Die Lösung über eine enum und static_casts halte ich für nicht gut, vielleicht bietet sich da das Visitor Pattern an.

    Uff...da versteh ich leider nur Bahnhof.

    Naja, ich habe die Fehler in der SW leider schon gemacht, und jetzt ist sie schon zu weit fortgeschritten als dass ich alles nochmal auf den Kopf stellen möchte.



  • Das Problem scheint ja zu sein, dass ein Channel irgendwann eine Operation ausführen muss, die nur ein bestimmter DeviceDriver implementiert. Wenn dein Design auf der Verwendung von abstrakten Interfaces und virtuellen Methoden darfst du natürlich nur gegen die Interfaces programmieren. Damit schaffst du eine enge Koppelung zwischen Channel und Driver. Ein anderer Ansatz (loose coupling) benutzt ein Eventsystem, das zwischen einzelnen Objekten (Channel, DeviceDriver) Nachrichten verschickt, wobei es jedem Objekt überlassen bleibt, wie es die entsprechende Nachricht verarbeitet. GUI Frameworks arbeiten z.B. so, dass sich ein Event Sink auf eine Event Source anmeldet und über Äbderungen informiert werden möchte. Je nach Event Typ müssen dann unterschiedliche Event Datentypen definiert werden, die die Nutzdaten des Events enthalten.

    Kurzes Pseudo-Code Beispiel:

    EventSource Source; // Datenquelle
    EventSinkA SinkA;   // Subscriber A
    EventSinkB SinkB;   // Subscriber B
    
    // SinkA und SinkB möchten informiert werden, wenn sich Daten in der Datenquelle ändern
    Source.OnDataChanged.connect( SinkA ); 
    Source.OnDataChanged.connect( SinkB );
    
    EventSource::SetData( const SomeData& Data )
    {
       // Daten übernehmen
       Data_ = Data;
    
       // alle Subscriber benachrichtigen
       OnDataChanged.fire( Data );
    }
    
    EventSinkA::operator()( const SomeData& Data )
    {
       // mach irgendwas mit den Daten
    }
    
    EventSinkB::operator()( const SomeData& Data )
    {
      // ignorier die Daten
    }
    

    Mit boost::signal und boost::function kann man member Funktionen von Objekten als Subscriber auf signal slots registrieren, sodass man so ein Framework bauen kann, in dem Funktionen/Methoden automatisch aufgerufen werden, wenn irgendwo ein Wert geändert (gelesen/empfangen) wurde. Das nachträglich einzubauen wird in deinem Fall wohl sehr aufwändig/langwierig sein, da du das Interface Design schon implementiert hast.

    Wenn man innerhalb einer Vererbungshierachie Datentypen von einer Basisklasse auf einen abgeleiteten Datentyp castet nennt man das (polymorphic) downcast. Genau zu diesem Zweck gibt es dynamic_cast, das man statt static_cast benutzen sollte.

    Das Besuchermuster/Visitor Pattern kann man einsetzen, um ohne casts von einem abstrakten Basistypen auf die konkrete abgeleitete Klasse kommen kann. Es ist allerdings nicht immer sinnvoll einsetzbar, aber das musst du für dich entscheiden. Wikipedia kann dir bestimmt einige Hinweise für die Benutzung geben 😉



  • DocShoe schrieb:

    Wenn dein Design auf der Verwendung von abstrakten Interfaces und virtuellen Methoden darfst du natürlich nur gegen die Interfaces programmieren. Damit schaffst du eine enge Koppelung zwischen Channel und Driver.

    Ja so ist's bei mir eigentlich der Fall. Es hat ja jeder Channel seinen eigenen Driver. Kein Driver kann von mehreren Channels verwendet werden.

    DocShoe schrieb:

    Ein anderer Ansatz (loose coupling) benutzt ein Eventsystem, das zwischen einzelnen Objekten (Channel, DeviceDriver) Nachrichten verschickt, wobei es jedem Objekt überlassen bleibt, wie es die entsprechende Nachricht verarbeitet.

    Ich schätze ich kenne das als Observer-Entwurfsmuster...aber in _glaube_ in meinem Fall wär's nicht geeignet gewesen.

    DocShoe schrieb:

    Wenn man innerhalb einer Vererbungshierachie Datentypen von einer Basisklasse auf einen abgeleiteten Datentyp castet nennt man das (polymorphic) downcast. Genau zu diesem Zweck gibt es dynamic_cast, das man statt static_cast benutzen sollte.

    Hm, ja, ich bin einer von den Leuten die nie ganz wissen wann sie den dynamic_cast einsetzen sollen...der geht ja auch in bestimmten Fällen.
    In gewisser Weise kümmere ich mich im obigen Beispiel selbst drum dass der static_cast nicht schiefgehen kann. Bau ich im aktuellen Programm Mist, würde mir der dynamic_cast in meinem Programm auch nicht wirklich viel weiterhelfen. Okay, beim Abschmieren mit einer unbekannten Exception sollte ich dann noch eine Fehlermeldung von catch(...) ins Logfile kriegen. Ein Fehler beim static_cast wäre ein Quellcode-Fehler und würde beim Testen beim Programmstart mit einem Absturz auf sich aufmerksam machen.

    DocShoe schrieb:

    Das Besuchermuster/Visitor Pattern ...

    schau ich mir vielleicht nochmal an...

    Danke an alle für die ganzen Tipps.


Log in to reply