Statische Vererbung via CRTP - wie funktioniert es genau?



  • Hallo,

    bei dynamischer Vererbung ist der Zugriff auf eine abgeleitete Klassen über einen Zeiger der Basisklasse möglich. Dieser Basisklassen Zeiger kann dann als Membervariable einer Klasse den Zugriff auf alle abgeleiteten Klassen bereitstellen.

    Wird wird Letzteres nun in CRTP umgesetzt? Mir ist nicht klar, wie dort ein Basisklassen Zeiger implementiert wird, über den auf alle abgeleiteten Klassen zugegriffen werden kann.

    template< class Derived >
    class Figur
    {
    public:
    	void Zeichne()
    	{
    		return static_cast< Derived * >(this)->ZeichneImpl();
    	}
    };
    
    class Dreieck : public Figur< Dreieck >
    {
    public:
    	void ZeichneImpl()
    	{
    		cout << "Dreieck\n";
    	}
    };
    
    class Quadrat : public Figur< Quadrat >
    {
    public:
    	void ZeichneImpl()
    	{
    		cout << "Quadrat\n";
    	}
    };
    
    class Zeichnung
    {
    	Figur* figur;  // Wie kann hier ein Basisklassenzeiger implementiert werden?
    
    	ErzeugeDreieck()
    	{
    		figur = new Dreieck();
    	}
    
    	ErzeugeQuadrat()
    	{
    		figur = new Quadrat();
    	}
    
    	void Zeichne()
    	{
    		figur->Zeichne();
    	}
    }
    

    Die Basisklassen Zeiger Membervariable in der Klasse Zeichnung funktioniert so wie hier geschrieben natürlich nicht. Wie wird so etwas mit CRTP implementiert?



  • Was du mit dynamischen Vererben meinst, heißt dynamische Bindung.

    Aber zu deiner Frage:
    Figur<T> ist ein komplett eigener Typ der auch all diese Eigenschaften hat. Daraus folgt also auch, dass du keinen allgemeinen Pointer für Figur herleiten kannst.
    Du müsstest eine neue Basisklasse anlegen, welche die Funktionen auf welche du generisch drauf zugreifen willst, kapselt und Figur<T> von dieser erben lassen.



  • Mr.Long schrieb:

    Figur<T> ist ein komplett eigener Typ der auch all diese Eigenschaften hat. Daraus folgt also auch, dass du keinen allgemeinen Pointer für Figur herleiten kannst.
    Du müsstest eine neue Basisklasse anlegen, welche die Funktionen auf welche du generisch drauf zugreifen willst, kapselt und Figur<T> von dieser erben lassen.

    Meinst Du mit "erben lassen" dynamische Bindung? Genau die wollte ich mit CRTP aus Performance Gründen (zeitkritische Grafikanwendung) umgehen.

    Ich hatte wiederholt gelesen, das CRTP als Ersatz für Vererbung benutzt werden kann. Nur ist mir nicht klar, wie das genau umgesetzt wird.

    Folgendes habe ich bereits herausgefunden:

    template <typename T>
    void Funktion_Zeichne(Figur<T>& f)
    {
    	f.Zeichne();
    }
    
    void main()
    {
    	Figur<Dreieck> *F = new Dreieck();
    	F->Zeichne();  // => Ausgabe Dreieck O.K.
    
    	Funktion_Zeichne(*F); // => Ausgabe Dreieck O.K.
    
    	cin.get();
    }
    

    Über "Funktion_Zeichne(Figur<T>& f)" ist eine der Vererbung ähnliche Funktionalität gegeben. Ist das schon alles, was CRTP an Vererbung ähnlichem Verhalten zur Verfügung stellt? Oder kann CRTP auch in eine Klasse eingebunden werden? Wie im Beispiel oben im Eingangsposting in der Klasse "Zeichnung"?



  • mireiner schrieb:

    Mr.Long schrieb:

    Figur<T> ist ein komplett eigener Typ der auch all diese Eigenschaften hat. Daraus folgt also auch, dass du keinen allgemeinen Pointer für Figur herleiten kannst.
    Du müsstest eine neue Basisklasse anlegen, welche die Funktionen auf welche du generisch drauf zugreifen willst, kapselt und Figur<T> von dieser erben lassen.

    Meinst Du mit "erben lassen" dynamische Bindung?

    Ja.

    CRTP als Ersatz für Vererbung benutzt werden kann
    

    Das stimmt imo. nicht. Höchstens bei Vererbung ohne dynamischer Bindung.

    CRTP ist dafür nicht geeignet. Du wirst immer ne virtuelle Funktion brauchen.

    Aber nochmal genauer erklät 🙂

    Das hier:

    template< class Derived >
    class Figur
    {
    public:
        void Zeichne()
        {
            return static_cast< Derived * >(this)->ZeichneImpl();
        }
    };
    
    class Dreieck : public Figur< Dreieck >
    {
    public:
        void ZeichneImpl()
        {
            cout << "Dreieck\n";
        }
    };
    
    class Quadrat : public Figur< Quadrat >
    {
    public:
        void ZeichneImpl()
        {
            cout << "Quadrat\n";
        }
    };
    

    Ist das Selbe wie:

    class FigurDreieck
    {
    public:
        void Zeichne()
        {
            return static_cast< Dreieck * >(this)->ZeichneImpl();
        }
    };
    class FigurQuadrat
    {
    public:
        void Zeichne()
        {
            return static_cast< Quadrat* >(this)->ZeichneImpl();
        }
    };
    class Dreieck : public FigurDreieck
    {
    public:
        void ZeichneImpl()
        {
            cout << "Dreieck\n";
        }
    };
    
    class Quadrat : public FigurQuadrat
    {
    public:
        void ZeichneImpl()
        {
            cout << "Quadrat\n";
        }
    };
    

    Wie willst du hier aus FigurQuadrat und FigurDreieck ein Figur bekommen? 🙂
    Du hast hier CRTP verwenden um weniger schreibaufwand zu haben, das war's aber auch schon wieder.

    PS. wenn es wirklich um hochkritischen Grafikoperationen geht, musst du dir überlegen ob du dein Design ändern willst um die dynamische Bindung rauszuschmeißen.



  • Lass den Unsinn und mach das normal mit normaler Vererbung und virtuellen Methoden. Wenn es dir um Performance geht, markiere die Methoden als final. Damit kann der Compiler in fast allen Situationen devirtualisieren.



  • asy schrieb:

    Lass den Unsinn und mach das normal mit normaler Vererbung und virtuellen Methoden. Wenn es dir um Performance geht, markiere die Methoden als final. Damit kann der Compiler in fast allen Situationen devirtualisieren.

    Danke für den Hinweis. Leider hatte ich das schon getan. Mit MSVC 2013 brachte "final" aber keine Verbesserung. Da ich mit Qt arbeite, bin ich noch auf MSVC2013 festgelegt. Kann MSVC2015 besser devirtualisieren?



  • Mr.Long schrieb:

    Du hast hier CRTP verwenden um weniger schreibaufwand zu haben, das war's aber auch schon wieder...Wenn es wirklich um hochkritischen Grafikoperationen geht, musst du dir überlegen ob du dein Design ändern willst um die dynamische Bindung rauszuschmeißen.

    Meine erste Programmversion verwendet einfach "switch" Schalter und ein Typ Flag um zwischen den verschiedenen Figuren in der Methode "Zeichne" zu unterscheiden.

    Als ich das Programm dann mit einfacher Vererbung umschrieb, hatte ich bei den Grafikanimationen einen Performance Verlust in der Debug Version von 25% und im Release von 33%. Zudem liefen die Grafikrotation nicht mehr wie zuvor in gleichmäßiger Geschwindigkeit, sondern ruckelten leicht.

    Nach einer Recherche im Netz las ich dann von CRTP als möglichen Ersatz für Vererbung bei zeitkritischen Anwendungsteilen. Nun sagst Du, dass CRTP dafür nicht geeignet ist und es besser ist ein anderes Design zu verwenden.

    Sollte ich es dann bei der "switch / flag" Methode belassen. Die sieht nicht sehr elegant aus, ist aber sehr schnell und flüssig. Oder gibt es noch bessere Design Methoden für zeitkritische Programmteile?



  • Profile dein Programm und vergleiche mit der Methode vorher. Ich bezweifle dass es an der Vererbung liegt.



  • Ich arbeite mit Qt und dem Qt Creator unter Windows mit dem MSVC2013 Compiler. Da gibt es für den Qt Creator keinen Profiler.

    Bin mir auch nicht sicher, ob ein Profiler weiterhelfen würde. Denn ich habe einfach nur eine "switch" Anweisung mit 10 Einsprüngen durch eine Basisklasse mit 10 Ableitungen ersetzt. Die Funktion "Zeichne", die die verschiedenen Grafiken zeichnet, ist unverändert geblieben. Viel zu analysieren gibt es da nicht.

    Mein Beispiel hier mit Rechteck und Quadrat ist nur ein Minimalbeispiel. Die jeweiligen "Figuren" sind im Programm wesentlich komplexer.



  • mireiner schrieb:

    Meine erste Programmversion verwendet einfach "switch" Schalter und ein Typ Flag um zwischen den verschiedenen Figuren in der Methode "Zeichne" zu unterscheiden.

    Als ich das Programm dann mit einfacher Vererbung umschrieb, hatte ich bei den Grafikanimationen einen Performance Verlust in der Debug Version von 25% und im Release von 33%. Zudem liefen die Grafikrotation nicht mehr wie zuvor in gleichmäßiger Geschwindigkeit, sondern ruckelten leicht.

    Genau das Gegenteil von dem was du gemacht hast machen Compiler, wenn sie es können, als Optimierung. Nennt sich "devirtualization".
    Also nicht wirklich verwunderlich dass es mit dem virtuellen Funktionsaufruf langsamer ist.
    Erstmal ist so ein virtueller Funktionsaufruf auch nicht GANZ billig, und vor allem verhindert er Inlining. Und durch das verhinderte Inlining oft noch weitere Optimierungen im "umgebenden" Code.

    mireiner schrieb:

    Nach einer Recherche im Netz las ich dann von CRTP als möglichen Ersatz für Vererbung bei zeitkritischen Anwendungsteilen. Nun sagst Du, dass CRTP dafür nicht geeignet ist und es besser ist ein anderes Design zu verwenden.

    Sollte ich es dann bei der "switch / flag" Methode belassen. Die sieht nicht sehr elegant aus, ist aber sehr schnell und flüssig. Oder gibt es noch bessere Design Methoden für zeitkritische Programmteile?

    Am meisten würde hier vermutlich bringen die Entscheidung um was für eine "Form" es sich handelt aus der innersten Schleife rauszuziehen.
    Also statt

    for each thing
        switch (thing.kind)
            ...
    

    besser

    for each kind
        for each thing of that kind
            ...
    

    Wobei die "for each thing of that kind" Schleife natürlich nicht so implementiert sein darf dass man über alle Elemente drüberiteriert und die unpassenden überspringt. Wäre ja sonst wieder langsam. Heisst: du brauchst dazu eine Datenstruktur wo es ohne Overhead möglich ist nur über Elemente eines bestimmten Typs zu iterieren.
    Also z.B. ne multi_map<kind, thing> oder, vermutlich besser, ne map<kind, vector<thing>> .

    In der äusseren Schleife kann man dann auch wieder problemlos virtuelle Funktionen aufrufen, weil sie selten genug aufgerufen werden.



  • Ich habe mich jetzt dazu entschieden in den zeitkritischen Grafikteilen bei der "switch" Methode zu bleiben, d.h. dort keine virtuellen Funktionen zu verwenden. Weil die "switch" Methode schnell in meinem Programm ist und einwandfrei funktioniert. Nur werde ich sie noch ein wenig umschreiben, um sie eleganter zu gestalten.



  • roflo schrieb:

    ...Ich bezweifle dass es an der Vererbung liegt.

    Du hattest recht. Mir ließ es keine Ruhe, dass die Grafikrotationen allein durch Vererbung so langsam und ruckelig geworden waren. Deshalb habe ich die kritischen Programmteile mit einfacher Vererbung nochmals umgeschrieben. Nun läuft die "switch" Version nur noch ca. 5% schneller und die Grafikrotationen minimal flüssiger. Die zuvor berichteten 33% Unterschied zu Lasten der Vererbung gingen auf mein Konto, aufgrund schlechter Architektur. Warum die Version mit Vererbung minimal Ruckelt (weniger flüssig läuft) bliebt ein Rätsel.



  • mireiner schrieb:

    Ich habe mich jetzt dazu entschieden in den zeitkritischen Grafikteilen bei der "switch" Methode zu bleiben, d.h. dort keine virtuellen Funktionen zu verwenden.

    löl



  • Tja. Zwei (oder mehr) Änderungen auf einmal zu testen und dann davon ausgehen zu wissen welche einen bestimmten Effekt verursacht hat ... da kann man sich schnell täuschen.


Log in to reply