Problem mit Software Architektur (C++)


  • Administrator

    Ishildur schrieb:

    Kannst du mir das erklären, irgendwie sehe ich das nicht so ganz...

    Ein Destruktor ist grundsätzlich auch nichts anderes wie eine Funktion. Wenn du keinen virtuellen Destruktor hast und auf der Basisklasse das delete aufrufst, so wird nur der Destruktor der Basisklasse aufgerufen und die von den abgeleiteten Klassen nicht. Somit wird das Objekt nur unvollständig zerstört.

    Das sind aber absolute C++ Grundlagen 😉

    Ist es eigentlich bei dir üblich, dass du des öfteren Klassen hast, welche nur pure virtual Funktionen haben?

    Grüssli


  • Administrator

    Ishildur schrieb:

    OK, soweit so gut, aber die destruktoren sind doch alle leer?

    Der Kompiler kann aber eigener Code reinsetzen, was er auch tut. Um Vererbung, virtuelle Funktionen usw. usf. lauffähig zu bekommen, braucht es normalerweise noch etwas zusätzlichen Code. Solcher Code wird dann auch im Destruktor aufgeräumt, ohne dass du es zu sehen bekommst 😉

    Grüssli



  • Ja ich meine auch mehr, wieso es dann nur bei der virtuellen Verwerbung zu einem Problem kommt?

    @Dravere

    Ist es eigentlich bei dir üblich, dass du des öfteren Klassen hast, welche nur pure virtual Funktionen haben?

    Ja, wieso? Nicht gut?


  • Administrator

    Ishildur schrieb:

    Ja ich meine auch mehr, wieso es dann nur bei der virtuellen Verwerbung zu einem Problem kommt?

    Es kann auch sonst zu Problemen kommen, es ist einfach undefiniertes Verhalten, wenn du es nicht machst. Und undefiniertes Verhalten ist eben undefiniert. Es kann mal funktionieren oder abstürzen, die Welt kann explodieren oder ein Antimaterie-Universum dabei entstehen und somit das unsrige gleich mitreissen und am Ende war nichts mehr ... 😃

    Ishildur schrieb:

    Ist es eigentlich bei dir üblich, dass du des öfteren Klassen hast, welche nur pure virtual Funktionen haben?

    Ja, wieso? Nicht gut?

    Kann man sicherlich drüber streiten, aber dein bisherigen Codestil und deine Vorgehensweisen deute ich so, dass du von irgendeiner anderen Sprache schlecht beeinflusst wirst und probierst in C++ nicht C++ zu programmieren. Aber bisher waren es nur Indikatoren. Will also nichts unterstellen 😉

    Grüssli



  • @Dravere
    Ich lerne gerne dazu 🙂

    Also ich versuche in C++ objektorientiert und möglichst flexibel zu programmieren, so wie ich das in jeder anderen Sprache ebenfalls versuche. Mein Grundsatz ist, "Programmiere gegen Interfaces nicht gegen Implementationen". Daher die Interfaces. Beispiel: IResource, die sieht bei mir beim aktuellen Projekt folgendermassen aus:

    // *********************************** interface "IResource" **********************************
    // This interface defines a set of functionalities which must be implemented by every
    // Resource.
    // Author: Samuel Lörtscher
    // ********************************************************************************************
    class IResource{
    public:
     // ------------------------------------ interface methods ------------------------------------
     virtual void              Register(IResourceService *Service,IResourceReader *Reader) = 0;
     virtual void              Load()                                                     = 0;
     virtual void              Unload()                                                   = 0;
     virtual void              Reload()                                                   = 0;
     virtual string           *GetName()   const                                          = 0;
     virtual ResourceFamily    GetFamily() const                                          = 0;
     virtual IResourceService *GetOwner()  const                                          = 0;
     virtual IResourceReader  *GetReader() const                                          = 0;
     virtual uint32            RefCount()  const                                          = 0;
     virtual bool              IsLoaded()  const                                          = 0;
     virtual ~IResource(){}
     // -------------------------------------------------------------------------------------------
    };
    // ********************************************************************************************
    

    Jedem, der IResource benützt, kann es völlig egal sein, wie IResource diese Dinge intern organisiert. Ich muss bspw. einfach abfragen können, ob das Teil geladen ist oder. Ob er dann das in einem bool speichert, oder seinen Owner fragen muss oder was auch immer ist mir egal...

    Dazu kommt der ResourceReader (Auch wieder nur ein Interface [only pure virtual methods]), dieser muss einfach die methoden Open, Read, Size, Close machen können. Ob der noch einen WinSock benötigt oder ein FileHandle oder was weis ich, müssen alle die Programmkomponenten, welche IResourceReader in irgendeiner Form benutzen nicht wissen...

    Oder hast du das wegen anderen Dingen gemeint?



  • Mußt nur gelegentlich schauen, ob sich der Aufwand überhaupt gelohnt hat.


  • Mod

    Es ist halt ein bisschen komisch, da eine Klasse die nur pure virtual Funktionen hat, keinerlei Funktionalität hat. Sie gibt ein paar Namen vor, macht sonst aber nichts was diesen Namen Bedeutung gibt.

    Gegenfrage: Inwiefern unterscheiden sich diese beiden Klassen?

    class fahrzeug
    {
     virtual ~fahrzeug(){}
     virtual void beschleunigen()=0;
     virtual void abbremsen()=0;
    };
    
    class ballaballapingpong
    {
     virtual ~ballaballapingpong(){}
     virtual void fnvgkjengdjkfgvd()=0;
     virtual void fdjfn4j4nwksdfnkd()=0;
    };
    

    Gar nicht, oder?

    Erst wenn man ein bisschen Funktionalität dazu packt, bekommt die Fahrzeugklasse einen Sinn:

    class fahrzeug
    {
     virtual ~fahrzeug(){}
     virtual void beschleunigen()=0;
     virtual void abbremsen()=0;
     virtual void einfache_fahrt
     {
       beschleunigen();
       abbremsen();
     }
    };
    


  • Ishildur schrieb:

    "Programmiere gegen Interfaces nicht gegen Implementationen"

    Nur haben die Interfaces in diesem Satz mit den Interfaces in Java nichts zu tun 😉


  • Administrator

    @Ishildur,
    Nehmen wir zum Beispiel die Methode GetName . Ich gehe mal davon aus, dass jeder Ressource einen Namen hat. Wieso speicherst du dies dann nicht gleich in der Basisklasse ab? Lohnt es sich wirklich hier eine virtuelle Funktion zu haben? Wer wird das schon anders abspeichern? Siehst du da wirklich sinnvolle Einsatzzwecke?
    Eine nicht ganz so offensichtliche Überlegung könnte man bei der Methode Reload machen. Ist ein Reload nichts anderes als ein Unload gefolgt von einem Load ? Ist es wirklich nötig, dass Reload virtuell ist? Könnte man nicht bereits eine Implementation anbieten, welche halt automatisch Unload und danach Load aufruft?
    So kann man sich halt jeweils weiterhangeln. Man ist zwar vielleicht sehr flexibel, aber womöglich erhöhst du für dich nur unnötig den Aufwand. Zudem ist da eigentlich noch diese Sache, dass man per Richtlinie keine virtuellen Funktionen als Schnittstelle haben sollte, sondern die virtuellen Funktionen immer private machen sollte. Aber das ist dann wirklich ein Streitpunkt 🙂

    Ansonsten siehe noch die Aussagen von SeppJ und Shade Of Mine, dann muss ich dies nicht auch noch hinschreiben 🙂

    Übrigens: Wieso gibst du bei GetName das std::string Objekt als Zeiger zurück? Ist auch einer der Indikatoren, dass du anscheinend ziemlich viel mit new anlegst.

    Grüssli


  • Administrator

    Dravere schrieb:

    Zudem ist da eigentlich noch diese Sache, dass man per Richtlinie keine virtuellen Funktionen als Schnittstelle haben sollte, sondern die virtuellen Funktionen immer private machen sollte. Aber das ist dann wirklich ein Streitpunkt 🙂

    Sorry für den Doppelpost, aber hatte vorhin den Link einfach nicht mehr gefunden dazu:
    http://www.gotw.ca/publications/mill18.htm

    Grüssli



  • @SeppJ

    Es ist halt ein bisschen komisch, da eine Klasse die nur pure virtual Funktionen hat, keinerlei Funktionalität hat.

    Das ist ja genau der Sinn 🙂

    Stell die folgende Situation vor:
    Du hast ein fertiges Projekt. Nun kommt der Chef oder der Kunde und sagt, wir müssen nun noch ein umfangreiches Logging System einfügen, der Zustand von einigen Objekten muss nun geloggt werden können. "LORD, wie soll denn dass gehen, davon hat beim Design der Architektur niemand was gesagt!"

    Kein Problem:

    class ILoggable{
    public:
     virtual string *LogStatus() = 0;
    };
    
    Logger{
    public:
     void RegisterLoggable(ILoggable *Loggable);
    };
    

    Nun kann man jedes objekt in der ganzen Applikation Loggable machen, indem es einfach das Loggable interface implementiert, oder in C++ virtuell erbt.
    Der Logger kann nun den Status von jedem Objekt in seiner Applikation zu jedem Zeitpunkt loggen ohne diese Objekte in irgendeiner Weise kennen zu müssen, ist das nicht Toll? 😃



  • Weiterer Vorschlag und auch passend zu Dravere's NVI Anmerkung:
    Hier sieht z.B. IsLoaded() nicht unbedingt danach aus, dass die pure virtual sein muss.
    Gegenvorschlag:

    class IResource{
    public:
       void Load();
       bool IsLoaded();
    private:
      bool loaded;
      void virtual doLoad();  // Wobei void bei Load komisch ist, regelst du das über Exceptions, wenn's nicht klappt?
    };
    ...
    void IResource::Load()
    {
      doLoad();
      loaded = true;  // what ever, bzw. besser je nach Rückgabewert setzen
    }
    bool IResource::IsLoaded()
    {
      return loaded; // what ever
    }
    


  • @Dravere
    Da hast du zu 100% recht, daher schreibe ich auch immer eine Default Teilimplementierung, bspw. AbstractResource, welche genau diese Aufgaben übernimmt. Für jede Resource kann man schliesslich selbst entscheiden, ob man von der AbstractResource Klasse erben will, welche einen Grossteil der "in den meisten Fällen identisch implementierten Teile" bereits erledigt, oder ob es bessär ist, eine komplett neue Implementation zu schreiben.



  • Ishildur schrieb:

    Nun kann man jedes objekt in der ganzen Applikation Loggable machen, indem es einfach das Loggable interface implementiert, oder in C++ virtuell erbt.

    Irgendwie hab ich das Gefühl, du hast nicht verstanden, wozu virtual Vererbung in C++ da ist.



  • @Jockelx
    Es gibt vielleicht Resourcen, bei denen das nicht ganz so einfach ist. Einige haben vielleicht Dependencies, dann müsste IsLoaded auch noch prüfen, ob diese alle auch geladen sind. Vielleicht ist eine Resource auch nur ein Container für andere Resourcen, welche geladen sein müssen, dann wäre das bool für die Katz. Eine andere Resource muss vielleicht zusätzlich eine Netzverbindung prüfen usw. Es gibt unzähliche Möglichkeiten und nicht immer kann das in einem simplen bool nachgesehen werden.



  • Irgendwie hab ich das Gefühl, du hast nicht verstanden, wozu virtual Vererbung in C++ da ist.

    Wie gesagt ich masse mir nicht an alles zu wissen und zu verstehen, ich lerne gerne dazu 🙂
    Es gibt natürlich probleme, wenn nun zwei Interfaces eine Methode mit demselben Namen sowie derselben Signatur anbieten. Bsp:

    class Loggable{
     public: virtual string *GetName() = 0;
    }
    
    class Scriptable{
     public: virtual string *GetName() = 0;
    }
    
    class MyObject:virtual public Loggable,virtual public Scriptable{
     public: string *GetName();
    }
    

    Ich denke, dafür is virtuelle Verwebung da, damit die Methode GetName nicht doppeldeutig ist. Ich will ja nicht, dass MyObjekt dem Logger und dem Skriper zwei unterschiedliche Namen zurückgibt.



  • Mmh, finde ich aber auch nicht sehr intuitiv, dass ein erfolgreiches 'Load' ein false bei 'IsLoaded' nach sich ziehen kann.
    War auch nur ein Beispiel, vielleicht passt es hier gerade wirklich nicht; häufig wäre das aber eine passende (passendere) Alternative.



  • Ishildur schrieb:

    Da hast du zu 100% recht, daher schreibe ich auch immer eine Default Teilimplementierung, bspw. AbstractResource, welche genau diese Aufgaben übernimmt.

    Und warum gibt es dann IResource wenn es eh AbstractResource gibt? Sollen doch alle davon erben, spart massig Aufwand.

    In Java macht man das so, weil es nicht anders geht, in C++ erbt man einfach von AbstractResource und gut.



  • Und warum gibt es dann IResource wenn es eh AbstractResource gibt?

    Damit man immer noch die Möglichkeit hat eine Implementierung zu schreiben, welche die Standardaufgaben anders löst als AbstractResource.



  • Ishildur schrieb:

    Und warum gibt es dann IResource wenn es eh AbstractResource gibt?

    Damit man immer noch die Möglichkeit hat eine Implementierung zu schreiben, welche die Standardaufgaben anders löst als AbstractResource.

    Sie ist doch abstract 😉


Anmelden zum Antworten