Ableitung durch Policy ersetzen



  • Hallo,

    ich habe ein Design mit einer abstrakten Basisklasse und zwei konkreten Unterklassen. Es ist aber so, dass ein Programm immer nur eine Unterklasse benutzt, abhängig davon wie es kompiliert ist. Also es kann zur Laufzeit nie sein, dass die andere Unterklasse benutzt wird. Daher ist es ziemlich sinnfrei, die virtuellen Funktionen mit mir herumzuschleppen, man könnte das stattdessen schon zur Kompilierzeit entscheiden. Daher möchte ich die konkrete Funktionalität der Unterklasse durch Policies realisieren.

    Der Ist-Zustand mal konkret in Code ausgedrückt:

    template <typename T, typename U = T>
    class Base {
    public:
        Base (int whatever):
        m_whatever(whatever)
        {}
    
        virtual ~Base() { /* machs kaputt */ }
    
        virtual void foo(const T& t) = 0;
    
    protected:
        int m_whatever
        // weitere Membervariablen, die von U und T abhängen, weggelassen
    };
    
    template <typename U, typename U = T>
    class ConcreteA : public Base<U,T> {
    public:
        ConcreteA(const std::string& bar, int whatever):
        Base(whatever),
        m_bar(bar)
        {}
    
        void foo(const T& t) { doSomething(t, this->whatever, m_bar); }   
    
    private:
       std::string m_bar;
    }
    
    template <typename U, typename U = T>
    class ConcreteB : public Base<U,T> {
    public:
        ConcreteB(float baz, int whatever):
        Base(whatever),
        m_baz(baz)
        {}
    
        void foo(const T& t) { doSomethingElse(t, this->whatever, m_baz); }   
    
    private:
       float m_baz
    }
    
    // irgendwo anders
    boost::ptr_vector<Base<int> > vector_of_stuff;
    #ifdef USE_A
    while(noch_daten_in_config_datei()) {
        std::string a_string = lese_string_aus_config_datei();
        vector_of_stuff.push_back(new ConcreteA(a_string, 4711);
    }
    #else
    while(noch_daten_in_config_datei()) {
        float b_float = lese_float_aus_config_datei();
        vector_of_stuff.push_back(new ConcreteB(b_float, 4711);
    }
    #endif
    
    vector_of_stuff[23]->foo(42);
    

    Das mit dem ifdef ist jetzt etwas übertrieben, aber im Grunde ist es genau so, dass sich schon zur Compile-Zeit entscheidet, dass vector_of_stuff nur außschließlich ConcreteAs oder ConcreteBs enthalten wird, nie aber beide gemischt.

    Daher ist es relativ sinnfrei, hier öffentliche Vererbung zu benutzen und bei jedem Aufruf von foo eine virtuelle Funktion zu dereferenzieren. Sinnvoller wäre, zur Compilezeit über Policies fetzulegen, was bei foo() gemacht werden soll.

    Der erste Ansatz dahingehend ist:

    template <typename FooPolicy, typename T, typename U = T>
    class Base : public FooPolicy{
    public:
        using FooPolicy::doFoo;
    
        Base (int whatever):
        m_whatever(whatever)
        {}
    
        ~Base() { /* machs kaputt */ }
    
        void foo(const T& t) { doFoo(t); }
    
    protected:
        int m_whatever
        // weitere Membervariablen, die von U und T abhängen, weggelassen
    };
    
    template <typename U, typename U = T>
    class APolicy {
    public:
        APolicy(const std::string& bar, Base<U,T>& base):
        m_base(base),
        m_bar(bar)
        {}
    
        void doFoo(const T& t) { doSomething(t, m_base->whatever, m_bar); }   
    
    private:
       std::string m_bar;
    }
    
    template <typename U, typename U = T>
    class BPolicy {
    public:
        BPolicy(float baz, Base<U,T>& base):
        m_base(base),
        m_baz(baz)
        {}
    
        void doFoo(const T& t) { doSomethingElse(t, m_base->whatever, m_baz); }   
    
    private:
       float m_baz
    }
    
    // irgendwo anders
    std::vector<Base<A_Policy, int> > vector_of_stuff;
    
    vector_of_stuff[23].foo(42);
    

    Dabei habe ich im Grunde zwei Probleme: Erstens, der Zugriff auf die in Base gespeicherten Daten. Dazu müsste ich zunächsts einmal die Policies zu "friends" von Base machen, damit sie m_whatever benutzen können. Das ist ja nicht weiter schlimm. Aber auf Ojektebene müssen sie ihr Base-Objekt ja auch kennen, daher der Konstruktor mit Base& base.

    Wie aber sieht jetzt der Base-Konstruktor aus?
    Angenommen, Base besitzt ein Objekt der Policy, dann müssten die Base-Konstruktoren ja so aussehen:

    Base (const std::string& bar, int whatever):
    policy(*this, bar),
    m_whatever(whatever)
    
    Base (float baz, int whatever):
    policy(*this, baz),
    m_whatever(whatever)
    

    Das ist aber in zweifacher Hinsicht Blödsinn, denn erstens kompiliert es nicht, da es ja immer nur einen Policy-Konstruktor gibt, der ENTWEDER einen String ODER ein float annimmt, jenachdem welche Policy man benutzt,
    und zweitens ist mir nicht wohl dabei, in der Initialisierungsliste des Konstruktors den this-Pointer zu dereferenzieren und an jemanden weiterzugeben. Im this-Pointer steht zu dem Zeitpunkt doch wahrscheinlich noch bit-Müll, oder?

    Okay, ich hoffe ich habe das Problem verständlich dargestellt.
    Die Grundfrage ist also: wie refaktorisiert man abgeleitete Klassen in Policies, wenn sie Daten der Basisklasse benutzen UND subtil unterschiedliche Konstruktoren haben?

    Philipp



  • Suche mal nach dem "Curiously Recurring Template Pattern"...



  • Davon habe ich schonmal was gehört, ja.

    Das ist

    class Base: public Derived<Base>
    

    oder?



  • PhilippM schrieb:

    Davon habe ich schonmal was gehört, ja.

    Das ist

    class Base: public Derived<Base>
    

    oder?

    Jap, und das ermöglicht quasi statische Polymorphie.



  • Habe das mal eingebaut im Beispielcode.

    Aber irgendwie macht die Kombination von Policies und CRTP keinen Sinn, da sich dann eine Kreisvererbung ergibt.

    Hier der Code:

    #include <string>
    #include <vector>
    
    template <typename FooPolicy, typename T, typename U = T>
    class Base : public FooPolicy{
    public:
        using FooPolicy::doFoo;
    
        Base (int whatever):
        m_whatever(whatever)
        {}
    
        ~Base() { /* machs kaputt */ }
    
        void foo(const T& t) { doFoo(t); }
    
    protected:
        int m_whatever;
        // weitere Membervariablen, die von U und T abhängen, weggelassen
    };
    
    template <typename CRTPBase, typename T, typename U = T>
    class APolicy : public CRTPBase {
    public:
        APolicy(const std::string& bar):
        m_bar(bar)
        {}
    
        void doFoo(const T& t) { doSomething(t, this->whatever, m_bar); }
    
    private:
       std::string m_bar;
    };
    
    template <typename CRTPBase, typename T, typename U = T>
    class BPolicy: public CRTPBase {
    public:
        BPolicy(float baz):
        m_baz(baz)
        {}
    
        void doFoo(const T& t) { doSomethingElse(t, this->whatever, m_baz); }
    
    private:
       float m_baz;
    };
    
    // irgendwo anders
    Base<APolicy<typename Base, int>, int> stuff(12);
    

    gcc 4.4 versteht ihn nicht:

    ../crtp-test/main.cpp:49:32: error: wrong number of template arguments (1, should be 3)
    ../crtp-test/main.cpp:23:7: error: provided for ‘template<class CRTPBase, class T, class U> class APolicy’
    ../crtp-test/main.cpp:49:38: error: template argument 1 is invalid
    ../crtp-test/main.cpp:49:45: error: invalid type in declaration before ‘(’ token
    

    und ich ehrlich gesagt gerade auch nicht.
    Base<APolicy<Base> > ist doch eigentlich Base<APolicy<Base<APolicy< ....
    Da habe ich doch eine seltsame Schleife konstruiert, oder???

    Philipp



  • Du sollst es ja auch nicht mehr über Policies machen, sondern wieder Deinen ersten Ansatz nehmen. Nur eben mit statischer anstatt mit dynamischer Polymorphie. Sonst wird es schwierig, mit den unterschiedlichen Konstruktoren...



  • Achtung:

    class Base: public Derived<Base>
    

    ist natürlich verkehrtherum.
    es muss

    class Derived : public Base<Derived>
    

    heissen.

    Dann hast du statt

    template <typename CRTPBase, typename T, typename U = T>
    class APolicy : public CRTPBase {
    

    einfach ein

    template <typename T, typename U = T>
    class APolicy : public Base<APolicy, T, U> {
    

    APolicy weiss ja wer seine Base ist.
    Und Base erbt natürlich nicht von APolicy.
    Sondern sieht nur so aus:

    template <typename FooPolicy, typename T, typename U = T>
    class Base{
    

    Base weiss aber dass es in Wirklichkeit ein FooPolicy ist und kann daher folgendes machen:

    void foo() {
      static_cast<FooPolicy*>(this)->doFoo();
    }
    

    um eine virtuelle doFoo funktion zu simulieren.

    PS:
    da Base ja über FooPolicy zugriff auf T und U bekommen kann braucht es selber diese garnicht als template parameter...



  • Ha!
    So geht's:

    template <typename Policy, typename T, typename U = T>
    class Base {
    public:
    
        Base (int whatever):
        m_whatever(whatever)
        {}
    
        ~Base() { /* machs kaputt */ }
    
        void foo(const T& t) { static_cast<Policy*>(this)->doFoo(t); }
    
    protected:
        int m_whatever;
        // weitere Membervariablen, die von U und T abhängen, weggelassen
    };
    
    template <typename T, typename U = T>
    class APolicy : public Base<APolicy<T,U>, T, U> {
    public:
        APolicy(const std::string& bar, int whatever):
        Base<APolicy, T,U>(whatever),
        m_bar(bar)
        {}
    
        void doFoo(const T& t) { doSomething(t, this->whatever, m_bar); }
    
    private:
       std::string m_bar;
    };
    
    template <typename T, typename U = T>
    class BPolicy: public Base<BPolicy<T,U>, T, U> {
    public:
        BPolicy(float baz, int whatever):
        Base<BPolicy, T, U>(whatever),
        m_baz(baz)
        {}
    
        void doFoo(const T& t) { doSomethingElse(t, this->whatever, m_baz); }
    
    private:
       float m_baz;
    };
    
    // irgendwo anders
    APolicy<int> stuff("Hallo", 42);
    

    Bleibt die Frage:
    Wann setze ich denn dann Policies ein, wenn statische Polymorphie durch CRTP erreicht wird?

    Philipp



  • @ShadeOfMine: GMTA oder FSD ? 😃



  • PhilippM schrieb:

    Bleibt die Frage:
    Wann setze ich denn dann Policies ein, wenn statische Polymorphie durch CRTP erreicht wird?

    Policies haben ja weniger mit der herkömmlichen Polymorphie zu tun, sondern mehr mit dem Strategy Design Pattern. Policies bieten dir die Möglichkeit teile der Klasse unterschiedlich zu konfigurieren.

    Ein Beispiel von Policies sind zB die allocatoren in der STL. Wenn diese auch etwas grottig sind. char_traits von Strings sind ein anderes Beispiel.

    Du nimmst Policies also zum verändern eines Teils des Verhaltens. Während du Vererbung einsetzt um das Verhalten zu erweitern.

    Natürlich können die Anwendungszwecke verschwimmen. zB ist in .NET eine SynchronizedList von List abgeleitet, obwohl man Synchronization auch als Policy einbauen könnte. Wo Ableitung aber Probleme macht ist, wenn man 2 Teile konfigurieren will. zB Synchronization und OwnerShip der Elemente in der Liste (soll die Liste selber das delete machen, wie boost::ptr_list oder sich wie std::list verhalten).

    Man müsste für Vererbung nun eine SynchronizedList und eine OwningList und einen OwningSynchronizedList erstellen. Mit Policies macht man einfach: List<Synchronized, Owning>.

    Was man durch Policy Based Design aber verliert ist die Polymorphie.

    PS:
    Wohl eher: ich poste zu langsam oder du denkst zu schnell 😉



  • Shade Of Mine schrieb:

    Policies haben ja weniger mit der herkömmlichen Polymorphie zu tun, sondern mehr mit dem Strategy Design Pattern. Policies bieten dir die Möglichkeit teile der Klasse unterschiedlich zu konfigurieren.
    Was man durch Policy Based Design aber verliert ist die Polymorphie.

    Danke! Das ist der Denkansatz, der mir gefehlt hat.

    Jetzt hab ich aber noch eine low-level Frage: Was bringt mir das fürn Vorteil gegenüber den virtuellen Funktionen?

    Bei virtellen Funktionen muss ich den Funktionspointer dereferenzieren, hier muss ich

    static_cast<Policy*>(this)->doFoo(t);
    

    ebenfalls dem Pointer folgen, um an die Funktion zu kommen.

    Wo ist da jetzt der Laufzeitvorteil der "Compilezeitpolymorphie"?

    Philipp



  • PhilippM schrieb:

    Jetzt hab ich aber noch eine low-level Frage: Was bringt mir das fürn Vorteil gegenüber den virtuellen Funktionen?

    Bei virtellen Funktionen muss ich den Funktionspointer dereferenzieren, hier muss ich

    static_cast<Policy*>(this)->doFoo(t);
    

    ebenfalls dem Pointer folgen, um an die Funktion zu kommen.

    Wo ist da jetzt der Laufzeitvorteil der "Compilezeitpolymorphie"?

    Virtuelle funktionen kosten mehr als nur eine zeiger dereferenzierung.
    Der cast hier beim crtp ist aber gratis, static_cast ist das in der regel. Es wird ja nur gesagt: das ist ein foo.

    Bei virtuellen funktionen hast du ein dynamisches dispatching da this ja auf alles zeigen kann. Und sobald du eine virtuelle funktion hast brauchst einen rattenschwanz an runtime type identification (um imer wissen zu koennen was this ist). Weitesr kann man virtuelle funktionen nicht gut inlinen.

    Klingt jetzt als waeren virtuelle funktionen sehr teuer, sind sie aber nicht. In java ist zb jede funktion virtuell und es ist kein problem. Es ist also eine minimale optimierung, mehr nicht.



  • Super, danke für die Erklärung.
    Na, dann bin ich ja mal auf den nächsten Profile-Lauf gespannt. Die "foo" funktion wird nämlich auf ~500 Objekten 50 mal pro Sekunde aufgerufen. Ob sich da was bemerkbar macht?

    Philipp



  • PhilippM schrieb:

    Ob sich da was bemerkbar macht?

    Poste dann bitte die Ergebnisse.
    Performance haengt an vielen Faktoren. Schneller wird es sicher sein, aber ob es merkbar schneller ist - das wird spannend.

    PS:
    mein letzter Post war in der UBahn geschrieben, nicht dass ich sonst so eine tolle Rechtschreibung haette, aber *brr* sieht der grottig aus.
    *Note to self* In der UBahn nur lesen, nicht schreiben.



  • Jetzt habe ich aber wieder ein Problem:

    Vorher:

    class FooInt {
    public:
        FooInt(Base<int>* base_pointer):
        m_base_pointer(base_pointer);
    private:
        Base<int>* m_base_pointer;
    };
    

    hängt ja jetzt vom konkreten Typ ab.

    Daher jetzt:

    template <template <typename T> class Concrete>
    class FooInt {
    public:
        FooInt(Concrete<int>* data);
    private:
        Concrete<int>* m_concrete_pointer;
    };
    

    Wenn ich den Konstruktor für int implementieren will:

    template <template <> class Concrete<int> >
        FooInt<Concrete<int> >::FooInt(Concrete<int>* data):
                m_concrete_pointer(data)
        {
        }
    

    Fliegt mir alles um die Ohren:

    error: expected identifier before ‘>’ token
    error: expected ‘>’ before ‘<’ token
    error: type/value mismatch at argument 1 in template parameter list for ‘template<int <anonymous> > class Concrete’
    expected a constant of type ‘int’, got ‘int’
    error: template argument 1 is invalid
    type/value mismatch at argument 1 in template parameter list for ‘template<int <anonymous> > class Concrete’
    expected a constant of type ‘int’, got ‘int’
    error: declaration of template ‘template<template<int <anonymous> > class Concrete> int FooInt(int*)’
    

    😮

    Hilfe!
    Muss ich den Base<int>* pointer jetzt durch einen Concrete<int>* pointer ersetzen oder durch einen Base<Concrete<int>, int>* Pointer?



  • Disregard.

    So, das Refaktoring ist durch. Meine Fresse, hab ich da wieder write-only Code produziert.

    Ich zitiere mal zusammenhanglos einen Konstruktor, der heute entstanden ist:

    template <template <typename T, typename U=T> class SimAccess, typename Packet, typename Metadata, typename SimType, typename CalcType = SimType, typename BusType = CalcType>
        class SendableData : public TypelessSendableData<Packet, Metadata> {
        public:
            SendableData(SimAccess<SimType, CalcType>* sim_data,
                         PrioType priority = Constant,
                         SimAccess<int>* suspend_ref = 0,
                         SimAccess<int>* dyn_length_ref = 0):
            m_sim_data(sim_data),
            m_priority(priority),
            m_force_outdated(false),
         ....
    

    Man gebe sich mal die länge der Template-Argumente.
    Aber erster positiver Effekt: Das Binary ist etwa 5% kleiner geworden!

    Bin mal gespannt aufs Laufzeitverhalten.

    Philipp


Anmelden zum Antworten