Dicken Constructor auslagern + Vererbungsmöglichkeit erhalten?



  • Hi,

    Mein Problem ist heute: Ich habe einen dicken Constructor.

    Konkret: Ich habe eine Modelklasse, welche ein 3D-Modell repräsentiert und dementsprechend eine Masse von Daten hält. Diese werden über eine externe Bibliothek geladen.

    Das ist etwas viel, um es in einen Constructor zu stecken, oder? Ich weiß nicht genau wieso, aber ich glaube, ein ctor sollte nicht so viel enthalten.

    Angenommen, der sollte das wirklich nicht, dann weiß ich aber nicht, wie ich das geschickt auslagern soll. Ich hab an eine Art Factory Method gedacht: Man hat eine statische Methode und die erzeugt einem halt das Objekt, natürlich mit privatem Constructor.

    Das hat aber den dicken Nachteil, dass ich viel Funktionalität von C++ aufgebe. Allein die Vererbung wird ziemlich ekelhaft.

    Jetzt habe ich mir überlegt: Warum möchte ich vererben? Antwort ist, dass ich Klassen davon erben lassen möchte, welche auf die gleiche Art und Weise geladen werden, evtl. aber zusätzliche Attribute oder auch Methoden haben können.

    Ich würde also gerne den ctor entlasten, zusätzlich aber die Möglichkeiten behalten, in den Derivaten den Creator (wen auch immer) der Basisklasse aufzurufen, weil ja 90% der Daten so bleibt.

    Beispiel:
    Mein Model soll z.B: an ViewedModel vererben. Dieses lebt im Ggs. zu dem Model in einer Kamerawelt und wird beobachtet, was man in OpenGL so umsetzt, dass nicht die Kamera sich bewegt, sondern sich das Objekt andersrum bewegt, sodass es Sinn macht, dass das Objekt sich selbst seine Koordinaten speichert bzw. diese in Abhängigkeit von einer Kamera berechnet.

    Da kann es jetzt aber sein, dass ich einen anderen ctor brauche als in der Oberklasse. Wenn ich über eine statische Methode das Model erstellen lasse, kann ich in ViewedModel diese ja nicht nutzen, es sei denn, ich biete darin ebenfalls eine statische Methode an:

    ViewedModel* ViewedModel::CreateModel(Camera* camera)
    {
    ViewedModel* newModel = Model::CreateModel(); // Das wäre eigentlich sogar ein Upcast und so gar nicht möglich, oder? Aber die Idee wird vll. klar.
    // Mach halt irgendwas mit der Kamera und dem Model
    return newModel;
    }
    

    Wäre evtl. mit richtigerem Code möglich, aber eigentlich gibt es das jetzt nicht als Pattern, was mich etwas zweifeln lässt... Ist es in C++ eigentlich üblich bei dem ganzen Factoryzeug zum Entladen den destructor zu nutzen, während die Erzeugung aber nicht in einem ctor sondern in so einer statischen Factory-Method passiert?

    Und gibt es in C++ einfach bessere Möglichkeiten, eine Factory zu implementieren als das, was ich hier fabriziere?

    Hoffe, mein Problem ist klar geworden, im Grunde zwei Fragen:

    Frage 1:
    Ist es richtig, dass ein ctor nicht so enorm viel enthalten sollte, wie das umständliche Laden etc? Es ist bislang noch nicht geplant, dass ein Model anders geladen wird - falls relevant für die Frage - allerdings würde eine Erweiterung in diese Richtung auf das Abstract Factory Pattern hinauslaufen und dann wird's mit der Vererbung wirklich schwierig.

    Frage 2:
    Angenommen, es stimmt, dass der ctor nicht so dick sein soll. Wisst ihr dann, wie ich die ausgelagerte Erstellung des Objektes von der Restfunktionalität trennen kann und trotzdem Vererbungen bereitstellen kann?

    Ich habe wieder Mal das Gefühl, ich mach mir das Leben schwerer als es ist... Freue mich auf jede Antwort und danke allein für's lange Lesen! 🙂



  • Hi,

    Okay, ich glaube, ich habe mein Problem wieder Mal umständlicher dargestellt als es ist, oder? Oder ist es einfach besonders schwierig? Mir erscheint das als ein Anfängerproblem, irgendwie. Ich will nicht hetzen, ich stelle nur meine Fragen oft etwas umständlich, daher wäre eine kurze Rückmeldung, falls ich's nochmal formulieren soll, hilfreich. 😉

    Meine derzeitige Lösung lässt den ctor jetzt erstmal fett. Das bietet irgendwie mehr Vorteile als alles andere.



  • Okay, ich habe eine Idee, die mir ganz gut gefällt:

    #include <string>
    
    class Model;
    
    using namespace std;
    
    class ModelLoader
    {
    public:
    void LoadModel(const string& filename, Model* model);
    };
    
    class ModelLoader1
    {
    public:
    Model* LoadModel(const string& filename, Model* model)
    {
    // Impl
    }
    };
    
    // Evtl. weitere Modelloader, aber da gibt's bisher ja keine
    
    class Model
    {
    private:
    // ...
    public:
    Model(const ModelLoader* modelLoader, const string& filename)
    {
    modelLoader.LoadModel(filename, *this);
    }
    };
    
    class ViewedModel : public Model
    {
    private:
    // ...
    Model(const ModelLoader* modelLoader, const string& filename, /* weitere Parameter */) : Model(modelLoader, filename)
    {
    // Zusätzliche Impl
    }
    };
    

    Mal alles zusammen geschmissen, hoffe das ist so verständlich.

    Diese Möglichkeit hat jetzt einige Vorteile:
    + Ich beschneide mich nicht in meinen Vererbungsmöglichkeiten, der ctor übernimmt die eigentliche Aufgabe nach wie vor und alles geht
    + Verschiedene ModelLoader sind möglich
    + Bei der Klassendefinition und -implementierung eines Models muss der ModelLoader noch nicht bekannt sein
    + Durch Verzicht auf statische Polymorphie ist das Model auch als Basisklasse containerfähig

    Nachteile:
    - Der Loader befüllt das Model nur über die Setter/Getter, alternativ wäre friend möglich
    - Es benötigt eine zusätzliche Loaderklasse, die auch außerhalb des Models instanziiert werden muss
    - Der ModelLoader befüllt ein vorhandes Objekt nur und erzeugt kein eigenes; der Nachteil hier bei ist mir allerdings nicht bewusst

    Gibt sicher weitere Nachteile... Was haltet ihr von dieser Methode?



  • Je mehr Arbeit verrichtet wird, umso höhere Chancen hast du dass irgendwo eine Exception fliegt oder sonst was nicht passt. Das kann am Ende ein nicht brauchbares (kaputtes) Objekt zur Folge haben. In C++ ist die schönste Möglichkeit, ein kaputtes Objekt erst gar nicht aufkommen zu lassen, ein Exception werfender Konstruktor -> kein Objekt.

    Willst du das nicht, oder kannst du solche Fehler beheben, ist eine (virtuelle) load-Methode eine gute Wahl. Ist load() nach dem Konstruieren Pflicht, ist eine Factory eine gute Wahl (virtuelle Funktionen im Konstruktor aufrufen ist bööööse ;)).

    Grundsätzlich kannst du auch mal schauen, ob deine Model-Klasse nicht gerade zur Gott-Klasse mutiert. Kannst du evtl. die Model-Klasse sinnvoll in kleinere logische Einheiten (Hilfsklassen) aufteilen?



  • Hi,

    Ja, ich habe auch schon überlegt, ob das zur Gottklasse entartet. Aber es gibt einfach sehr viele Informationen, die ein Objekt betreffen. Die Klasse hat viel Zeug, was nur zum Konstruieren gebraucht wird, deswegen macht es Sinn, Model zu entschlacken und das Laden halt in eine Konstruktionsklasse auszulagern.

    load() ist Pflicht beim ctor, aber wie gesagt, hat eine Factory den Nachteil, dass die Vererbung von Model nicht mehr so einfach wird. Oder könntest Du beschreiben, wie man das geschickt macht? Denn in der Factory hat man normalerweise ja den Fall, dass es viele verschiedene Objekte gibt, die auf der gleichen Vererbungsstufe stehen, sprich: nicht voneinander erben können. Bei mir soll aber ein ViewedModel die Erweiterung des Models sein, während beides seine Berechtigung hat und instanziiert werden kann.

    Was ist denn gegen meine Lösung im zweiten Post einzuwenden? Hier erhalte ich mir den ctor und lager die Funktionalität aus.

    Und wieso ist es böse virtuelle Methoden im ctor aufzurufen? 🙂 Weil man durch Base- + Derivate-ctor sonst Gefahr läuft, eine Funktionalität zwei Mal aufzurufen? Könnte ich das nicht mit dem Aufruf in Base: this->Base::method() verhindern? Gut, das machen zu müssen ist auch nicht schön...

    PS: Ich will jetzt doch, dass es mehrere Erzeuger geben kann... es würde zumindest Sinn machen.



  • Eisflamme schrieb:

    load() ist Pflicht beim ctor, aber wie gesagt, hat eine Factory den Nachteil, dass die Vererbung von Model nicht mehr so einfach wird.

    Ich verstehe nicht wie du darauf kommst.

    class Base {
      Base() {}
      virtual void load() {}
    public:
      static Base* create() {
        Base* b = new Base;
        b->load();
        return b;
      }
    };
    
    class Derived : public Base {
      Derived() {}
      virtual void load() {}
    public:
      static Derived* create() {
        Derived* d = new Derived;
        d->load();
        return d;
      }
    };
    
    int main() {
      std::vector<Base*> vec;
      vec.push_back(Derived::create());
      vec.push_back(Base::create());
      Derived* d = Derived::create();
      // usw
    }
    

    Was ist daran jetzt mit Vererbung nicht vereinbar? Factory heißt ja eben, dass für alles was erzeugt werden soll, eine eigene Factory-Methode/Klasse angeboten werden soll.

    Und wieso ist es böse virtuelle Methoden im ctor aufzurufen? 🙂 Weil man durch Base- + Derivate-ctor sonst Gefahr läuft, eine Funktionalität zwei Mal aufzurufen? Könnte ich das nicht mit dem Aufruf in Base: this->Base::method() verhindern? Gut, das machen zu müssen ist auch nicht schön...

    Nein, das ist nicht der Grund. Du könntest auf die Idee kommen, dass du einfach eine virtuelle Methode "load" anbietest, die das Laden übernimmt. load muss in allen Derivaten implementiert werden. Dass die Sache leichter wird, rufst du load() im Base-Konstruktor auf. Was aber hier passiert: Es wird IMMER die Base::load() aufgerufen, auch wenn du ein Derived-Objekt instantiierst. Grund: Im Konstruktor von Base hast du nur ein Base-Objekt - this zeigt auf ein Base. Google mal, dann findest du auch ein paar Beispiele (oder schreib dir selber ein Minimalbeispiel :P)

    Was ich mich aber schon die ganze Zeit frage:
    Du weißt, dass wenn du ein Derived instantiierst, auch der ganze Konstruktor von Base durchgelaufen wird. Sind denn diese Initialisierungen auch direkt für Derived nutzbar, oder wird da dann wieder wild herumgeändert, bis es für Derived passt?



  • Ich weiß halt nicht, was der Begriff Factory jetzt alles umfasst. Laut Wiki gibt's halt Factory Method und Abstract Factory, zwei verschiedene Konzepte. Das hier ist dann die Factory Method, ok... Aber als ich das in nem andren Thread postete, hieß es, das sei ziemlich unschön...

    Na ja und an dem Loader steckt auch ein Rattenschwanz an Strukturen etc. Möglicherweise wäre es nicht schlecht, wenn ich das halt komplett auslagere. Es kommt jetzt wie gesagt wohl doch noch ein zweiter Loader dazu und dann wäre die Abstract Factory ja ganz gut. Aber dann bräuchte ich ja quasi ne konkrete Factory für Base und eine für Derivated... Und eigentlich müsste die Derivated-Factory auch die Base-Factory nutzen, sonst hab ich redundanten Code. Da ne Idee?

    Es wird IMMER die Base::load() aufgerufen, auch wenn du ein Derived-Objekt instantiierst. Grund: Im Konstruktor von Base hast du nur ein Base-Objekt - this zeigt auf ein Base. Google mal, dann findest du auch ein paar Beispiele (oder schreib dir selber ein Minimalbeispiel :P)

    Ja, aber in Derivate ruft er doch den richtigen ctor auf, oder? Beispiel:

    class Base
    {
    private:
    virtual void load() {}
    public:
    Base() {Base::load();}
    };
    
    class Derivate : public Base
    {
    private:
    virtual void load() {}
    public:
    Derivate() : Base() {load();}
    };
    

    Hier erwarte ich bei Instanziierung von Derivate, dass er beide loads aufruft.

    Aber eigentlich macht es doch am meisten Sinn, load() gar nicht erst virtual zu machen. Schließlich ist es ja durchaus so gedacht, dass der Base::ctor vorher aufgerufen wird mit all den Konsequenzen (Aufruf von load). Das ist bei Vererbung ja so beabsichtigt und genau so wäre das hier auch korrekt.

    Aber eigentlich ist doch die ctor-Methode mit Schmeißen einer Exception am schlausten für den Fall, dass ich eben nur einen Creator habe und dieser relativ einfach gestrickt ist, oder? (Was wie gesagt beides nicht der Fall ist)

    Du weißt, dass wenn du ein Derived instantiierst, auch der ganze Konstruktor von Base durchgelaufen wird. Sind denn diese Initialisierungen auch direkt für Derived nutzbar, oder wird da dann wieder wild herumgeändert, bis es für Derived passt?

    Base ist halt Model und Derivate ViewedModel. Model speichert lediglich die Modellinformationen. ViewedModel (werde ich wohl noch in TransformedModel umbenennen) speichert aber ebenfalls Positions/Rotationsdaten, da es im Ggs. zum Model positioniert werden kann. Das wiederum geschieht, weil es durch einen Renderer, der Objekte und Kameras kennt, entsprecht transformiert werden soll...
    D.h. ViewedModel ist durchaus eine Erweiterung des Models und nicht etwas anderes. Wäre es etwas anderes, würde ich ja den gemeinsamen Nenner in eine Basisklasse auslagern und nicht ViewedModel von Model erben lassen. 🙂

    Model benötige ich trotzdem, da man das Ding auch außerhalb des Renderer-Kontextes benutzen können soll, das schafft dem "Benutzer" (das bin wieder nur ich) der "Engine" mehr Möglichkeiten.



  • *Push*

    Hab das Gefühl, die Experten tun sich mit meinem Thread schwer, finden den uninteressant. Hab ich beim Posten was falsch gemacht? 🙂



  • Naja, irgendwie versteh ich Dein Problem nicht ...

    Wiederverwenden von Code sollte nie zu Vererbung führen ! genau das führt nämlich zu den "Gottklassen" (schoener Ausdruck, btw). Für Wiederverwendung gibt es andere Techniken. Deshalb verstehe ich, und die anderen vielleicht auch, das gewusel wegen der load() Methode im Konstruktor nicht !

    Bleibt also die Frage ... warum leitest Du ab ? zeig mal wie deine Klassenhirarchie aussehen soll, und wo du sie "virtuell" also austauschbar verwenden willst ?

    Weiterhin, Dir schlaegt der "umfangreiche" Konstruktor aufm Magen. ok.
    Solange der Code aber im "Constructor" deiner Instanzen laeuft, wird sich laufzeittechnisch nix aendern. Ob du den dann in mehrere methoden/funktionen etc auslagerst oder nicht, dient dann "nur" der Übersichtlichkeit.

    Deswegen, wie Du selbst erkennst, hasst du 2 grundlegende Möglichkeiten.

    1. du rufst Deine Instanzen mit entsprechenden Konstruktoren und unaufbereiteten Daten auf, die dir das ganze zeugs gleich in interne Daten umwandelt.
    Vorteil: deine Innere Datenstrukturen koennen recht komplex sein ... und keiner muss die wissen.
    Nachteil: der Konstruktor kann exeptions werfen. Du musst drauf bauen das alles korrekt zurueckgerollt wird, du musst code aufwenden um faktisch exceptionneutral zu bleiben ... das ist nicht ganz einfach.

    2. du baust Dir factory Methoden oder Classen, die die aufbereitung der Daten fuer dich uebernehmen. Der Konstruktor der Klasse wird dann gleich mit den aufbereiteten daten befuellt.
    Vorteil: dein Konstruktor kann recht naiv sein. Exeptions sollten da faktisch nimmer auftreten. Du wirst nie "ungueltige" klassen haben, weil die factory so bauen kannst das sie bei Problemen nix nach aussen gibt.
    Nachteil: du brauchst Datenstrukturen die an den Konstruktor uebergibst. Wenn die gross sind kann das performance kosten (kopien). Normal sollten aber solche konstruktoren immer in zeitunkritischen momenten aufgerufen werden.
    Auf der anderen seite, wenn da auch details verstecken willst, kann das auch wesentlich mehr code erfordern ...

    Was genau fuer Dich am besten ist, musst du aber selbst wissen.

    Ausm Gefühl raus würd ich sagen eher die Factory Methode. Ich verwend tendentiell auch mehr Factories als so komplexe CTors. und ich liebe initialisierungslisten, meist is der Ctor rumpf eh leer bei mir.

    Aber ab und an komm ich auch nicht drumherum, und ich verwend recht viele und komplexe statische methoden, um die basis-Daten fuer eine Klasse zu bekomnmen.

    Das entscheidet die Situation ...

    Ciao ...


  • Mod

    Eisflamme schrieb:

    Hab das Gefühl, die Experten tun sich mit meinem Thread schwer, finden den uninteressant. Hab ich beim Posten was falsch gemacht? 🙂

    Ich würde mich zwar selber nicht als Experten bezeichnen, aber ich finde die ganze Prämisse der Frage, dass ein Konstruktor nicht viel Arbeit verrichten sollte nicht richtig. Der Defaultkonstruktor sollte (falls es überhaupt einen gibt - deine Klasse klingt nicht danach) nicht viel tun, das ist richtig. Aber wenn ich dem Konstruktor zum Beispiel einen Dateinamen gebe, dann habe ich auch nichts dagegen, wenn er gigabyteweise Daten aus der Datei lädt. Wenn dies nötig ist, dann ist es eben so.

    Und ein Objekt wird auch nicht zum Gottobjekt, nur weil es mengenmäßig viele Daten hält. Dies ist auch ok. Es sollte nur nicht viele verschiedene Arten von Daten halten, ohne diese sinnvoll zu kapseln.



  • Hi,

    Okay, super! Vielen Dank fuer Zeit und Muehe des Antwortens!

    Ich glaube, mir war selbst noch nicht ganz bewusst, was mich stoert. Im Grunde genommen ist die Model-Klasse insofern unnoetig aufgeblaeht, als dass ich alleine fuer den Ladevorgang 6-7 private Methoden habe und zahlreiche Hilfsstrukturen. Was davon in der eigentlichen Datenstruktur von der Klasse landet, ist nur sehr wenig bzw. anders geartet. Ich konvertiere halt einen Riesenhaufen von Daten um (naemlich Modelldatei-Daten in mein Format der Klasse, womit ich dann arbeiten kann).

    Ich loesche alles Temporaere und die privaten Methoden sind nach aussen natuerlich auch nicht sichtbar, das sind also keine Probleme. Auch kann ich ausschliessen, dass die Konvertierung noch fuer andere Klassen genutzt wird, weil das sehr genau auf die Konvertierung von meinen Modelldateien auf mein Format in der Klasse zugeschnitten ist.

    Diese Masse an Methoden und Strukturen, die ich verwende, laesst halt die Headerdatei etwas unuebersichtlich erscheinen. Wenn ich das Model nutze, interessiert mich das alles im Nachhinein naemlich nicht mehr. Ist also eine "schoenheitstechnische" Frage. Jetzt kann man sich natuerlich fragen, ob diese Schoenheit so wichtig ist, da die Factory-Methoden auch andere Nachteile mit sich bringen.

    Und der zweite Teil ist jetzt:
    Ich habe fest gestellt, dass ich neben diesem Loader eigentlich noch einen anderen gebrauchen kann. Welchen ich benutze haengt jetzt von ein paar Dingen ab. Man koennte es dazu zusammenfassen, dass das Dateiformat (die Dateiendung) wichtig wird.
    Da wuerde ich jetzt halt zwei Funktionen basteln wie createModel(filename) und createViewedModel(filename) und die schauen sich die Endung an und basteln dann entweder mit Loader1 oder Loader2, die ich dann wohl jeweils in einer Klasse kapseln wuerde wegen den temporaeren Daten etc.
    Unschoen ist halt, dass ich fuer Model und fuer ViewedModel jeweils den gleichen Code zum Auswerten der Dateiendung zwei Mal haette. Daher wuerde sich doch eine Art load/fill-Methode in den Loaderklassen anbieten, die die Klassen einfach nur befuellt, diese koennte allerdings im ctor der Modelklassen aufgerufen werden, dann kann ViewedModel einfach den Model-ctor nutzen, die Modelklassen sind schlank und mehrere Loader sind moeglich. Voraussetzung ist, dass Model und ViewedModel im ctor der instanzierte Loader mitgegeben wird, durch das Loader-Interface muss aber die Implementierung nicht bekannt sein.

    Das war etwas wirr. Ist es verstaendlich, umstaendlich oder beides?



  • Diese Masse an Methoden und Strukturen, die ich verwende, laesst halt die Headerdatei etwas unuebersichtlich erscheinen.

    Dann schau dir mal "PIMPL" an.

    Und der zweite Teil ist jetzt:
    [...]

    In wieweit fügt ViewedModel nun dem Model neue Methoden hinzu? Willst du Model immer Polymorph verwenden? Oder brauchst du auch mal ein konkretes ViewedModel?



  • PIMPL wird demnaechst angeschaut! 🙂

    Model und ViewedModel koennen beide benutzt werden. Model ist halt ausserhalb des Kamerakontextes zu benutzen, den meine Engine anbieten wird, ViewedModel steht darin. Beide haben daher ihre Berechtigung, technisch ist ViewedModel aber eine Erweiterung in dem Sinne, dass es mit anderen Objekten interagiert und Positions/Rotations/Skalierungsdaten enthaelt und dafuer auch einige Methoden enthaelt.

    D.h. der Teil, welcher die eigentlichen Modelldaten laedt, ist gleich. Polymorphie ist hier eigentlich nie notwendig.



  • Hat das Objekt einen "natürlichen, leeren" Zustand, dann biete einen Default-Ctor an, der ein leeres Objekt erstellt.
    Hat das Objekt keinen solchen Zustand, dann biete eben keinen Default-Ctor an.

    Kann das Objekt von aussen ( public ) bearbeitet werden, dann kannst du die "Loader" komplett unabhängig davon implementieren. Der Loader legt sich dann einfach ein neues (leeres) Objekt an, und steckt über die public Schnittstelle die Daten rein.

    Kann das Objekt nicht von aussen bearbeitet werden (oder nicht vollständig), dann gibt es verschiedene Möglichkeiten. Die wohl einfachste, aber nicht immer beste, ist, dem Objekt selbst das Laden aufzubürden.

    Besser kann es gehen, wenn man trotzden "Loader" Klassen implementiert. Diese können dann nicht mehr über die public Schnittstelle mit dem Objekt kommunizieren, also werden sie friend .

    Eine andere Möglichkeit wäre, dass die Loader-Klassen nur eine "Bauanleitung" (Zwischenformat) für das Objekt erstellen, und mit dieser dann das neue Objekt erstellen. Wie gut das geht/sinnvoll das ist, hängt von der Art des Objekts ab. Manchmal ist die "Bauanleitung" genau so kompliziert wie das eigentliche Objekt, bzw. würde sogar 100% gleich aussehen. Dann macht die Unterscheidung keinen Sein. Manchmal geht es allerdings wieder recht gut. Beispiel: Texturen für ein 3D Spiel. Wenn wir HDR und komprimierte Texturformate mal ignorieren, dann könnte man aus "Bauanleitung" einfach ein Array aus 32 Bit Pixeln im RGBA Format verwenden. Der Loader würde also das entpacken von PGN, JPEG, ... übernehmen, und die Textur-Klasse das Konvertieren in das Farbformat der Grafikkarte und den Upload in den Texturspeicher.



  • Zu ein paar anderen Dingen...

    PIMPL hat mit der hier diskutierten Frage IMO nichts zu tun.

    Dass ein Ctor nur wenig tun sollte halte ich auch für eine sehr fragwürdige Richtlinie.

    Selbst dass der Default-Ctor nur wenig tun sollte halte ich für fragwürdig. Ich würde sagen der Default-Ctor sollte nichts tun, was nicht nötig ist, bzw. wofür es nicht einen guten Grund gibt, wenn es "teuer" ist. Beispiel für was ich meine: der Default-Ctor von std::vector sollte kein reserve machen.

    Andrerseits sollte ein Default-Ctor IMO alles nötige machen, um unnötige "halb initialisierte" Zustände zu vermeiden. Beispiel: eine Socket-Klasse sollte sich IMO im Default-Ctor ein Socket-Handle holen, um den "not-a-socket"/"null"/"empty" Zustand zu vermeiden, auch trotz dem das Holen eines neuen Socket Handles vermutlich ein paar (zig?) Tausend Zyklen "teuer" ist.



  • Zum Pimpl: Der primäre Zweck des Handle-Body-Idioms besteht darin, Compilezeit-Abhängigkeiten zu verringern. Durch die Auslagerung der Implementierung kann diese (d.h. auch Memberdeklarationen der Impl-Klasse) geändert werden, ohne Client-Code neu zu kompilieren. Zudem müssen die Headerdateien für Membervariablen nicht im Client-Code mitkompiliert werden.

    Eine Trennung im Sinne des Pimpl-Idioms teilt die Aufgabenbereiche der Klasse nicht wirklich auf mehrere Klassen auf, sondern lagert sie nur aus.



  • Okay. Cool, danke, ich geh Mal der Reihe nach durch 😃

    hustbaer:
    Das Wort natürlich scheint beim leeren ctor die Hauptrolle zu spielen, okay.

    Für mich besteht dann weiterhin hauptsächlich die Frage, was die Vorteile davon sind, den Loader auszulagern, wenn man nur ein erzeugtes Objekt (im Ggs. zu dem üblichen Anwendungsfall von Factoryfunktionen/klassen) hat. Mir fällt es schwer, da die Vorteile zu sehen, wenn man von schlankerem Header absieht, da es für einige Aspekte (z.B. Vererbung) ja doch praktisch ist, wenn das Objekt sich selbst erstellt.

    Ich bin ja auf der Suche nach der besten Lösung, aber nicht sicher, wie ich diese finden kann.

    Insgesamt habe ich den ctor jetzt glaube ich etwas besser verstanden. Ich glaube, ich muss einfach mehr in Objekten denken, keine Ahnung.

    Und wenn ich mehrere Erzeuger habe? Die Abstract Factory läuft im klassischen Klassendesign ja auch nicht so gut mit Vererbung, oder? Was haltet ihr denn dann von der Implementierung:

    class ModelDataLoader
    {
    public:
    void LoadModelData(Model* model, const string& str) = 0;
    };
    
    class ModelDataLoader1 : public ModelDataLoader
    {
    public:
    void LoadModelData(Model * model, const string& str);
    };
    
    class ModelDataLoader2 : public ModelDataLoader
    {
    public:
    void LoadModelData(Model* model, const string& str);
    };
    
    class Model
    {
    private:
    // attributes
    // mit friend zu ModelDataLoader
    public:
    Model() {} 
    };
    
    class ViewedModel : public Model
    {
    private:
    // attributes und zusätzliche attribute
    public:
    ViewedModel() {} 
    
    // zusätzliche setter für Attribute, die nur ViewedModel hat? (X)
    };
    
    int main()
    {
    typedef boost::scoped_ptr<Model*> ModelPtr;
    typedef boost::scoped_ptr<ViewedModel*> ViewedModelPtr;
    
    ModelDataLoader l1;
    ModelDataLoader l2;
    
    ModelPtr model1(new Model);
    ModelViewedPtr model2(new ViewedModel);
    
    l1.LoadModelData(model1);
    l2.LoadModelData(model2);
    
    return 0;
    }
    

    Also Loader1/2 hat jetzt nix mit Model/ViewedModel zu tun. Nachteil dieser Methode hier ist, dass ein Model eigentlich im leeren Zustand eigentlich sinnfrei ist. Vorteil ist aber, dass ich vererben kann wie blöd.

    1. Was haltet ihr davon?
    2. Eigentlich ist ViewedModel ein Model, was betrachtet werden kann. Man könnte aber auch formulieren, ein ModelView ist ein Model, das mit Positionsdaten versehen ist, d.h. ich könnte das auch über Komposition statt Vererbung lösen. Was haltet ihr von so was?

    Danke so weit!



    1. Eigentlich ist ViewedModel ein Model, was betrachtet werden kann. Man könnte aber auch formulieren, ein ModelView ist ein Model, das mit Positionsdaten versehen ist, d.h. ich könnte das auch über Komposition statt Vererbung lösen. Was haltet ihr von so was?

    Ich würde weder das eine noch das andere machen.

    Eher eine "ModelInstance" Klasse:

    class ModelInstance
    {
    public:
        ModelInstance(shared_ptr<Model> const& model, Matrix4x4 const& position /* ... */);
    
    private:
        shared_ptr<Model> m_model;
        Matrix4x4 m_position;
        // evtl. weitere Daten für Vertex-Blending, ...
    };
    

    Also ein Verweis auf ein Model + Position wo es zu sehen sein soll.



  • Scheiße, ja! Das macht ja sowieso Sinn, weil die Anzeige eines Models zum Model selbst ja 1 : n ist. Das muss ich mir nochmal durch den Kopf gehen lassen, danke. 🙂

    Und zu folgender Frage:

    Für mich besteht dann weiterhin hauptsächlich die Frage, was die Vorteile davon sind, den Loader auszulagern, wenn man nur ein erzeugtes Objekt (im Ggs. zu dem üblichen Anwendungsfall von Factoryfunktionen/klassen) hat.

    ?



  • Eisflamme schrieb:

    Für mich besteht dann weiterhin hauptsächlich die Frage, was die Vorteile davon sind, den Loader auszulagern, wenn man nur ein erzeugtes Objekt (im Ggs. zu dem üblichen Anwendungsfall von Factoryfunktionen/klassen) hat.

    ?

    Naja... pfuh.
    Kommt jetzt drauf an.
    Wenn das Datenformat wie es im File steht quasi nur eine 1:1 Serialisierung von dem ist wie die Daten im Speicher liegen, dann ist der Sinn erstmal fraglich.
    Was man immer heraustrennen sollte sind Dinge wie der "Formattierer" (bei Boost.Serialization übernehmen den Job IIRC die Archiv-Klassen), damit man z.B. wahlweise XML oder Binärdateien speichern/laden kann.

    Wenn das Datenformat im File nicht 1:1 dem enstpricht wie's im Speicher liegt, dann gehört das IMO in eine eigene Klasse. Die Model Klasse ist das Model, und kümmert sich um Model-Angelegenheiten. Dem Model kann es egal sein wie das .obj, .x, .fbx etc. File-Format aussieht.

    Die Aufgabe des Loaders/Parsert/... ist es dann jeweils "sein" File-Format zu kennen. D.h. du hast einen XFileLoader, einen ObjFileLoder, einen FbxFileLoader etc.

    Und selbst wenn du nur ein Dateiformat unterstützen willst/musst: das "dekodieren" eines bestimmten Formats hat IMO in der Daten-Klasse nichts verloren.

    Denk einfach an das Beispiel Textur- oder Image-Klasse. Die Textur- oder Image-Klasse sollte sich IMO nicht darum kümmern müssen wie ein PNG, JPEG, BMP etc. zu lesen geht. Das macht je eine eigene Klasse pro File-Format.


Anmelden zum Antworten