"Klassentyp" übergeben?



  • abstract factory pattern



  • af schrieb:

    abstract factory pattern

    Hat m.E. nix mit Klassenübergabe zu tun. factory pattern kapseln nur die Erzeugung. In dieser stehe ich dann ja vor dem gleichen Problem.



  • Vielleicht sowas?

    template <typename T>
    Base* Create()
    {
        return new T;
    }
    
    std::map<int, Base* (*)()> map;
    map[0] = &Create<MyClass>;
    map[1] = &Create<OtherClass>;
    

    Bei fortlaufenden Indizes kannst du auch std::vector nehmen.

    P.S.: Bitte hör mit diesem grausamen C-Präfix auf.



  • NewBee2 schrieb:

    Unter Delphi geht das mit "class of" und class als Typ.
    Wie ist das unter C++ / G++ ?

    C++ unterstützt Metaklassen, Klassenreferenzen und (virtuelle) Klassenmethoden nicht, insbesondere keine virtuellen Konstruktoren. Du mußt dir irgendeinen Workaround mithilfe von Templates ausdenken. Etwa so:

    (Nicht erschrecken, es ist etwas wild; laß dich einen Moment darauf ein, dann leuchtet ein, wieso es sinnvoll ist):

    template <class I> struct MetaInterface {};
    template <class I, class C> struct MetaClass : MetaInterface <I> {};
    
    struct BaseObject { ... }; // irgendeine Basisklase
    template <>
        struct MetaInterface<BaseObject>
    {
        virtual BaseObject* create (SomeType someParams) = 0; // quasi virtueller Konstruktor
        virtual bool supports (SomeType someCondition) = 0; // quasi virtuelle Klassenmethode
    };
    typedef MetaInterface<BaseObject> BaseInterface; // quasi "class of"
    
    template <class C> struct MetaClass<Base, C> : MetaInterface<Base>
    {
            // Standardimplementation des virtuellen Konstruktors
            // (man beachte die Kovarianz des Rückgabewertes)
        virtual C* create (SomeType someParams) { return new C (someParams); }
            // Standardimplementation der virtuellen Klassenfunktion
        virtual bool supports (SomeType someCondition) { return false; }
    };
    MetaClass<BaseObject, BaseObject> BaseClass;
    
    struct DefaultObject : public BaseObject
    {
        DefaultObject (SomeType someParams) { ... }
    };
    MetaClass<BaseObject, DefaultObject> DefaultClass; // benutzt obige Standardimplementation
    
    struct SpecializedObject : public BaseObject
    {
        SpecializedObject (bool somethingElse, SomeType someParams) { ... }
    };
    template <>
        struct MetaClass<BaseObject, SpecializedObject> : MetaClass<BaseObject, BaseObject>
    {
            // Spezialisierung des virtuellen Konstruktors für SpecializedObject
        virtual SpecializedObject* create (SomeType someParams) { return new SpecializedObject (false, someParams); }
            // Spezialisierung der Klassenfunktion
        virtual bool supports (SomeType someCondition) { ... }
    };
    MetaClass<BaseObject, SpecializedObject> SpecializedClass;
    
    BaseInterface* myClassReference = &SpecializedClass;
    BaseObject* myObject = myClassReference->create (someParams);
    

    Disclaimer: Code ist ungetestet.



  • Ich verstehe gerade nicht, warum das abstract factory pattern nicht genau das macht, was du willst?

    a = *CBasisKlasse ;
    a= (*CBasisKlasse) new MS[0].Klasse;
    

    würde dann so aussehen:

    a = MS[0].Factory->Create();
    

    wobei die Factories halt alle ein Objekt, das von CBasisKlasse abgeleitet wurde, zurückgeben (oder besser gesagt einen Zeiger darauf).

    Was genau soll das hier bedeuten?

    a = *CBasisKlasse ;
    


  • An Audacias Beispiel würde ich jetzt mal kritisieren, dass man vor lauter templates, Metas, Specializeds, Bases gar nicht mehr sieht, dass das dasselbe ist wie ein simples virtual factory pattern, nur mit templates, Metas, Specializeds und Bases.
    Aber wahrscheinlich übersehe ich da gerade auch etwas...



  • Warum überhaupt immer Klassen und virtuelle Funktionen mit möglichst viel Boilerplate-Code benutzen, wenn sich virtuelle Konstruktoren übersichtlich und mit einer Zeile Code pro abgeleiteter Klasse implementieren lassen?



  • Ich kann dir gerade nicht folgen, Nexus.

    Aber wie wär's mit []() -> Base* { return new Derived; }
    Ziemlich kurz! 😃



  • Decimad schrieb:

    Ich verstehe gerade nicht, warum das abstract factory pattern nicht genau das macht, was du willst?

    Weil Klassenreferenzen in Delphi viel mehr sind als nur irgendwelche Factories.

    Decimad schrieb:

    An Audacias Beispiel würde ich jetzt mal kritisieren, dass man vor lauter templates, Metas, Specializeds, Bases gar nicht mehr sieht, dass das dasselbe ist wie ein simples virtual factory pattern

    Offenbar habe ich dich tatsächlich soweit verwirrt, daß du nicht erkanntest, daß es nicht dasselbe ist 😉

    Mein Beispiel ist so umfangreich, weil ich damit versucht habe, einige der Möglichkeiten von Klassenreferenzen in Delphi nachzubilden. Und wie du schon festgestellt hast, ist es auch ein gutes Argument dafür, stattdessen einfach Delphi zu benutzen, wo man sich nicht explizit mit "Metas, Specializeds und Bases" herumschlagen muß. Aber ich will es gerne nochmal im Einzelnen vorstellen.

    Ich beginne mal mit dem Klassenreferenzen-Standardbeispiel: den Dateiformaten. Wir haben eine abstrakte Basisklasse Image , zu der es etwa die Unterklassen Bitmap , PNGImage und JPEGImage geben könnte. Das sieht zunächst so aus:

    // Delphi:
    type
      TImage = class
      public
        constructor CreateCopy(Image: TImage); overload; virtual; abstract;
        constructor CreateFromStream(Stream: TStream); overload; virtual; abstract;
        procedure SaveToStream(Stream: TStream); virtual; abstract;
      end;
      TBitmap = class (TImage)
      public
        constructor CreateCopy(Image: TImage); override;
        constructor CreateFromStream(Stream: TStream); override;
        procedure SaveToStream(Stream: TStream); override;
      end;
    
    // C++:
    class Image
    {
    public:
        virtual void saveToStream (std::ostream& stream) = 0;
    };
    class Bitmap
    {
    public:
        Bitmap (Image* image);
        Bitmap (std::istream& stream);
        virtual void saveToStream (std::ostream& stream);
    };
    

    Die handelsübliche Benutzung verläuft in Delphi und C++ gleichermaßen (Exceptionsicherheit ignorieren wir jetzt mal):

    function LoadBitmapFromFile (const FileName: String): TImage;
    var
      Stream: TStream;
    begin
      Stream := ...;
      Result := TBitmap.CreateFromStream (SomeFileStream);
    end;
    
    Image* loadBitmapFromFile (const std::string& fileName)
    {
        std::fstream stream (...);
        return new Bitmap (stream);
    }
    

    So einfach ist es natürlich meistens nicht; wir wollen ja mehrere Dateiformate unterstützen. Unsere Laderoutine soll in der Lage sein, das Dateiformat zu berücksichtigen. In Delphi:

    type
      TImageClass = class of TImage;
    
    function LoadImageFromFile (const FileName: String; ImageClass: TImageClass): TImage;
    var
      Stream: TStream;
    begin
      Stream := ...;
      Result := ImageClass.CreateFromStream (SomeFileStream);
    end;
    
    { bescheuertes Anwendungsbeispiel }
    function LoadBitmapFromFile (const FileName: String): TBitmap;
    begin
      Result := LoadImageFromFile (FileName, TBitmap) as TBitmap; { Hier wird eine Klasse als Argument übergeben! }
    end;
    

    In C++ könnten wir das natürlich mit einem Factory-Template nachbilden:

    typedef Image* (ImageStreamFactory) (std::istream& stream);
    template <class T>
        Image* imageStreamFactory (std::istream& stream)
    {
        return new T (stream);
    }
    
    Image* loadImageFromFile (const std::string& fileName, ImageStreamFactory factory)
    {
        std::fstream stream (...);
        return factory (stream);
    }
    
        // bescheuertes Anwendungsbeispiel
    Bitmap* loadBitmapFromFile (const std::string& fileName)
    {
        return &dynamic_cast<Bitmap&> (*loadImageFromFile (fileName, imageStreamFactory<Bitmap>));
    }
    

    Nun läßt sich der Bildtyp aber meist dem Dateinamen entnehmen; es sollte möglich sein, die Auswahl des Graphikformats zu automatisieren. Wir brauchen so etwas wie ein globales Verzeichnis aller verfügbaren Graphikformate, aus denen unsere Funktion selbst auswählen kann. In C++ wäre das mit obigen Factories folgendermaßen lösbar:

    class ImageRegistry
    {
    public:
        void registerImageFactoryForFileExt (const std::string& fileExt, ImageStreamFactory factory);
        ImageStreamFactory getByFileExt (const std::string& fileExt);
    };
    ImageRegistry theRegistry;
    
    void registerKnownImageClasses (void) // muß beim Start der Anwendung aufgerufen werden
    {
        theRegistry.registerImageFactoryForFileExt (".bmp", imageStreamFactory<Bitmap>);
        ...
    }
    
    Image* loadImageFromFile (const std::string& fileName)
    {
        std::fstream stream (...);
        return (theRegistry->getByFileExt (extractFileExt (fileName))) (stream);
    }
    

    Wir möchten aber außerdem noch eine generische Speicherroutine haben, die automatisch die richtige Dateiendung hinzufügt. Die Dateiendung ist ja nicht ein Spezifikum unserer ImageRegistry ; sie gehört zur Klasse Image dazu und sollte auch Image -Objekten bekannt sein. Also nehmen wir etwas Redundanz in Kauf und spendieren Image noch eine Methode, die die Dateiendung zurückgibt:

    class Image
    {
    ...
        virtual std::string getFileExt (void) = 0;
    };
    
    ...
    
    std::string Bitmap::getFileExt (void)
    {
        return ".bmp";
    }
    
    void saveImageToFile (Image* image, const std::string& fileName)
    {
        std::string fullName = fileName + image->getFileExt ();
        std::fstream stream (...);
        image->saveToStream (stream);
    }
    

    Eigentlich ist die Dateiendung aber eine Eigenschaft der Klasse Image , nicht eines Objektes von diesem Typ; wir wollen auch Bitmap::getFileExt() schreiben können. Aber dazu müßte getFileExt() statisch und virtuell zugleich sein.

    Tatsächlich geht das in Delphi:

    type
      TImage = class
      ...
        class function GetFileExt: String; virtual; abstract; // "class function" entspricht statischer Methode
      end;
    
      TClassRegistry = class
      public
        function FindFirstClass<T> (Predicate: TPredicate<T>): T;
        function FindClasses<T> (Predicate: TPredicate<T>): TArray<T>;
      end;
    
    var
      ClassRegistry: TClassRegistry;
    
    function FindImageClassByFileExt (const AFileExt: String): TImageClass;
    begin
      Result := ClassRegistry.FindFirstClass<TImageClass> (
        function (TheClass: TImageClass): Boolean
        begin
          Result := TheClass.GetFileExt = AFileExt; { GetFileExt() ist eine Klassenfunktion und kann ohne Instanz aufgerufen werden.
                                                      Zugleich ist es virtuell; was zurückgegeben wird, hängt davon ab, ob TheClass
                                                      auf TBitmap, TJPEGImage oder TPNGImage verweist. (So wie übliche Zeiger
                                                      Objektzeiger sind, ist TheClass ein Klassenzeiger.) }
        end);
    end;
    
    function LoadImageFromFile (const FileName: String): TImage;
    var
      Stream: TStream;
    begin
      Stream := ...;
      Result := FindImageClassByFileExt (ExtractFileExt (FileName)).CreateFromStream (Stream);
    end;
    
    procedure SaveImageToFile (Image: TImage; const FileName: String);
    var
      FullName: String;
      Stream: TStream;
    begin
      FullName := FileName + Image.GetFileExt; { Ebenso kann GetFileExt natürlich für Objekte aufgerufen werden; es verhält
                                                 sich auch hier wie eine virtuelle Funktion, abhängig vom Typ des Objektes. }
      Stream := ...;
      Image.SaveToStream (Stream);
    end;
    

    Wie man sieht, ist hier die ImageRegistry zu einer allgemeineren ClassRegistry geworden; es ist nicht nötig, daß der Registrierungsmechanismus von den Dateiendungen weiß oder überhaupt auf von Image abgeleitete Klassen beschränkt ist. Es gibt außerdem keine Redundanz bezüglich der Dateiendung: sie wird von der virtuellen Klassenmethode GetFileExt() zurückgegeben und muß nicht beim Registrieren der Klasse zusätzlich angegeben werden. Außerdem muß die Klasse nicht von Hand registriert werden; die ClassRegistry ist von selbst in der Lage, alle im Projekt definierten Klassen zu finden (das ist allerdings ein anderes Thema).

    Es gibt noch viele andere Möglichkeiten, von den Klassenreferenzen Gebrauch zu machen. Vielleicht möchten wir ein Duplikat einer Graphik im selben Format erstellen können:

    function CreateDuplicate (Image: TImage): TImage;
    begin
      Result := TImageClass (Image.ClassType).CreateCopy (Image);
    end;
    

    Oder wir beugen uns der Einsicht, daß Dateiendungen im realen Leben nicht immer mit dem Inhalt übereinstimmen müssen, und implementieren die Erkennung von Dateiendungen auf Signaturbasis (z.B. beginnen Bitmap-Dateien immer mit den Bytes "BM", bei PNG-Dateien steht "\x89PNG\x0D\x0A\x1A\x0A" etc.):

    type
      TImage = class
      ...
        class function SignatureMatches (Stream: TStream): Boolean; virtual; abstract;
      end;
    
    ...
    
    class function TBitmap.SignatureMatches(Stream: TStream): Boolean;
    var
      Sig: array[0..1] of AnsiChar;
    begin
      if Stream.Size < 2 then
        Exit (False);
      Stream.ReadBuffer (Sig, 2);
      Stream.Position := Stream.Position - 2;
      Result := (Sig[0] = 'B') and (Sig[1] = 'M');
    end;
    
    function FindImageClassBySignature (Stream: TStream): TImageClass;
    begin
      ImageClasses := ClassRegistry.FindFirstClass<TImageClass> (
        function (TheClass: TImageClass): Boolean
        begin
          Result := TheClass.SignatureMatches (Stream);
        end);
    end;
    

    Natürlich reichen die Factories vollkommen aus, wenn der OP nur einen einzigen virtuellen Konstruktor nachbilden will. Aber es ist vielleicht klargeworden, warum man schon zu ein paar "Metas" und "Specializeds" greifen muß, wenn man Klassenreferenzen in C++ nachbilden will.



  • lol, ich hab noch nie ernsthaft gehört, dass irgendwer einen Vorteil von Delphi ggü. C++ argumentieren möchte *duck und wegrenn*



  • Man sollte normalerweise nicht versuchen bestimmte Features von Sprache X in Sprache Y nachzubilden.
    Sondern lieber versuchen das was man eigentlich erreichen will in Y so zu implementieren wie es mit Y gut umzusetzen ist.



  • hustbaer schrieb:

    Man sollte normalerweise nicht versuchen bestimmte Features von Sprache X in Sprache Y nachzubilden.
    Sondern lieber versuchen das was man eigentlich erreichen will in Y so zu implementieren wie es mit Y gut umzusetzen ist.

    Sehe ich auch so. Ich bin seinerzeit nach einigem Experimentieren davon abgerückt, den oben erwähnten Workaround produktiv zu verwenden. Wenn ich Klassenreferenzen brauche, nehme ich Delphi.

    Wenn man in C++ nur einen einzigen virtuellen Konstruktor braucht, nimmt man eben Factories. Und wenn man so etwas wie mein Graphikformat-Beispiel machen möchte, wird man wahrscheinlich auch lieber auf die genaue Nachbildung verzichten und stattdessen zwei Klassenhierarchien parallel pflegen:

    class Image;
    class ImageType
    {
    public:
        virtual Image* createCopy (Image* rhs) = 0;
        virtual Image* createFromStream (std::istream& stream) = 0;
        virtual std::string getFileExt (void) = 0;
        virtual bool signatureMatches (std::istream& stream) = 0;
    };
    
    class Image
    {
    public:
        virtual void saveToStream (std::ostream& stream) = 0;
        virtual ImageType& getType (void) = 0;
    };
    
    class Bitmap;
    class BitmapType
    {
    public:
        virtual Bitmap* createCopy (Image* rhs);
        virtual Bitmap* createFromStream (std::istream& stream);
        virtual std::string getFileExt (void) { return ".bmp"; }
        virtual bool signatureMatches (std::istream& stream);
    };
    BitmapType bitmapType;
    
    class Bitmap
    {
    public:
        Bitmap (Image* image);
        Bitmap (std::istream& stream);
        virtual void saveToStream (std::ostream& stream);
        virtual BitmapType& getType (void) { return bitmapType; }
    };
    

    Die Nachteile sind gut tragbar, solange man nicht überbordenden Gebrauch von diesen expliziten "Typklassen" macht.



  • @audacia: 👍👍

    Und danke für den Exkurs über Klassenreferenzen, tut meinem Tellerrand gut!


Anmelden zum Antworten