Suche Möglichkeit für semi-statische Polymorphie
-
Hallo zusammen,
folgendes ist mein Problem: Ich muss derzeit ein schon vorhandenes Programm umschreiben. In diesem Programm kamen drei verschiedene Klassen von Objekten vor, die man auch von einer gemeinsamen Ursprungsklasse hätte ableiten können. Der ursprüngliche Programmierer hat dies aber nicht getan, wahrscheinlich weil ihm der Overhead durch virtuelle Funktionen zu viel war. Ich habe testhalber mal die Methoden die in der inneren Schleife vorkommen virtual gemacht und verglichen, das macht schon einen gewaltigen Unterschied. Da viele kleine Methoden benutzt werden ist der relative Overhead recht groß und Performance ist bei diesem Projekt recht wichtig.
Ich musste nun das Programm um vier weitere Klassen erweitern, die sich eigentlich auch von der gleichen Basisklasse ableiten ließen wie die schon vorhandenen. Da erstmal schnell eine lauffähige Lösung gefragt war, habe ich zunächst den ursprünglichen Stil beibehalten und die vier neuen Klassen von Hand in den Code eingefügt. Nun, da ich erstmal keinen Stress mehr habe kann ich aber etwas nachdenken:
Es ist durchaus wahrscheinlich dass später nochmals weitere Klassen hinzugefügt werden müssen. Weiterhin enthält das Projekt nun zahllose Stellen an denen Code doppelt (oder gar siebenfach!) vorkommt, was die Wartung äußerst schwierig macht. Gleichzeitig soll aber die Verwendung echter Polymorphie aus Performancegründen vermieden werden. Schönerweise ist aber zur Compilezeit bekannt wieviele unterschiedliche Klassen es geben soll.
Meine erst Idee wäre ja jetzt, mit Templates eine statische Polymorphie zu machen, um Codeabschnitte a la
for (i=typ1_liste.begin(), i!=typ1_liste.end(), ++i){ i->tu_was(); } for (i=typ2_liste.begin(), i!=typ2_liste.end(), ++i){ i->tu_was(); }durch sowas zu ersetzen:
schleife_fuer_tuwas<typ1>(typ1_liste); schleife_fuer_tuwas<typ2>(typ2_liste);Das ist aber noch nicht so schön, da man zum Hinzufügen einer neuen Klasse über den gesamten Quelltext gehen müsste um die entsprechenden Ausdrücke hinzuzufügen. Ich hätte da gerne eine flexiblere Struktur. Ich habe dazu mehrere Ideen:
a) Mit variadic templates könnte ich das wahrscheinlich machen. Ich habe mir die aber noch nicht genau genug angeschaut, weil ich nicht unbedingt davon ausgehen kann dass der Zielcompiler diese unterstützt. Außerdem sind sie noch nicht offiziell standardisiert und ich möchte auch dass mein Code noch läuft falls der neue Standard anders aussieht als in den jetzigen Implementierungen. Diese Lösung würde ich nur nehmen, falls mir jemand bestätigen kann, dass sie viel besser ist als meine anderen Überlegungen
b) Ich programmiere in einer eigenen Makrosprache und jage vor dem Compilieren einen eigenen Präprozessor über den Code der statt meines Makrocodes die entsprechenden Templateaufrufe abhängig von der Anzahl der benötigten Klassen einsetzt. Der normale C-Präprozessor ist wahscheinlich nicht mächtig genug für meine Zwecke, weil auch etwas kompliziertere Ausdrücke vorkommen würden in der Art von:
mache_eine_verschachtelte_Schleife_in_der_jede_Instanz_einmal_zusammen_mit_jeder_anderen_als_Funktionsargument_aufgerufen_wird(Funktionsname(Argument1,Argument2), Typ1, Typ2, Typ3)Sowas habe ich noch nie gemacht. Ist das eine gute Idee? Gibt es schon Makroprozessoren für sowas die ihr mir dafür empfehlen könnt?
c) Ich frage hier im Forum ob jemand bessere Vorschläge hat.
d) Ich schlucke den Produktivitätsverlust (nur ich weiß derzeit wieviel schneller das Programm sein könnte) und bevorzuge bessere Wartbarkeit durch Verwendung von dynamischer Polymorphie. An dieser Lösung stört mich aber, dass zur Complierzeit eigentlich alle nötigen Informationen vorhanden sind, um ein schnelleres Programm zu bekommen. Das Programm wird sehr oft sehr lange laufen und etwas mehr Mühe bei der Entwicklung würde sich bezahlt machen.
-
Was spricht denn gegen Vererbung ohne Polymorphie? Momentan hast du schliesslich auch mehrere Container, deren Typen zur Compilezeit feststehen. Also brauchst du gar keine virtuellen Funktionen.
Oder habe ich etwas Wichtiges übersehen?
-
Performance kann man im Bedarfsfall durch schnellere Hardware kaufen, Stabilität dagegen nicht. Wie Du schon erkannbt hast wird Dein gegenwärtiger Ansatz früher oder später im Code-Chaos enden, was in der Regel auf Kosten der Stabilität geht.
Zitat: Was nutzt Performance wenn sie lediglich bedeutet das das Programm schneller abstürzt?
-
loks schrieb:
Performance kann man im Bedarfsfall durch schnellere Hardware kaufen, Stabilität dagegen nicht. Wie Du schon erkannbt hast wird Dein gegenwärtiger Ansatz früher oder später im Code-Chaos enden, was in der Regel auf Kosten der Stabilität geht.
Zitat: Was nutzt Performance wenn sie lediglich bedeutet das das Programm schneller abstürzt?
Ich mag dieses "Premature Optimization"-Standardargument so langsam auch nicht mehr. Wie du vielleicht gelesen hast, wird das Programm lange laufen und Performance wäre lohnenswert.
Mehr Geld (das unter Umständen gar nicht vorhanden ist) für Hardware ausgeben, weil die Software schlecht programmiert ist, entspricht zwar leider der Philosophie vieler heutiger Computerspiele, ist aber deshalb noch lange kein guter Ansatz. Ich bin froh, dass es noch Leute wie SeppJ gibt, die sich auch Gedanken machen.
-
Verstehe ich das so richtig: Du hast verschiedene Klassen mit gemeinsamer Schnittstelle, die du derzeit noch nicht-polymorph hältst, also in einzelnen Containern? Jetzt hättest du gerne Template-Polymorphie, ohne aber für jeden Typ den Code erneut hinschreiben zu müssen und ohne jeweils eine kapselnde Template-Funktion zu schreiben?
-
Nexus schrieb:
Was spricht denn gegen Vererbung ohne Polymorphie? Momentan hast du schliesslich auch mehrere Container, deren Typen zur Compilezeit feststehen. Also brauchst du gar keine virtuellen Funktionen.
Oder habe ich etwas Wichtiges übersehen?
Korrekt, brauchen tue ich die virtuellen Funktionen nicht. Mein Problem ist eher, dass es leicht sein kann, dass neue Typen dazukommen. Ich such eine automatisierte Möglichkeit, diese in den Code einzufügen.
Momentan sieht's in etwa so aus(wie gesagt mit der heißen Nadel gestrickt, deshalb so umständlich):
class typ1{ // Gemeinsames Interface wird mit für Typ1 spezifischen Methoden implementiert }; class typ2{ // Gemeinsames Interface wird mit für Typ2 spezifischen Methoden implementiert }; ... { Anweisungsblock A, in dem jedesmal Typ1 verwendet wird; } { Anweisungsblock A, in dem jedesmal Typ2 verwendet wird; }Vorteile:
+ Schnell auszuführen
Nachteile:
- Wartungshölle, wenn man was an Anweisungsblock A ändern möchte
- Will man weitere Klassen hinzufügen ist jede Menge Copy-Paste angesagtDaraus könnte man aber leicht machen:
template<class T> void AnweisungsblockA(Argumente){ Anweisungsblock A, in dem jedesmal T verwendet wird } ... AnweisungsblockA<Typ1>(Argumente_Typ1); AnweisungsblockA<Typ2>(Argumente_Typ2);Vorteile:
+Genauso flott wie jetzt, da äquivalent
+Anweisungsblock A taucht nur 1x auf, ist also gut zu warten
Nachteile:
- Kommen neue Klassen hinzu, muss man überall sowas wieAnweisungsblockA<Typ3>(Argumente_Typ3);einfügen.
Eine Alternative wäre eben echte dynamische Polymorphie:
class gemeinsames_interface{ virtual interface_methoden() }; class typ1: public gemeinsames_interface{ // virtuelle Interfacemethoden werden für Typ1 spezifische implementiert }; class typ2: public gemeinsames_interface{ // virtuelle Interfacemethoden werden für Typ2 spezifische implementiert }; ... { Anweisungsblock A, in dem die TypX-Instanzen als Instanzen von geminsames_interface benutzt werden }Vorteile:
+ Genausogut, wenn nicht gar besser zu warten als die Templatevariante
+ Es ist ein leichtes neue Klassen hinzuzufügen -> Methoden definieren, in den gemeinsamen Container einfügen, fertig.
Nachteile:
- Deutlich langsamer, ärgerlich, da eigentlich alles Nötige bekannt ist für eine schnellere LösungWas ich daher suche, aber wo ich leider kein geeignetes Sprachmittel kenne wäre sowas:
#define VERWENDETE_KLASSEN Typ1,Typ2 ... Unbekanntes_Sprachmittel(AnweisungsblockA<X>, Argumente_X, VERWENDETE_KLASSEN) /* Soll zu etwas wie AnweisungsblockA<Typ1>(Argumente_Typ1); AnweisungsblockA<Typ2>(Argumente_Typ2); werden */Vorteile:
+ Schnell
+ leicht zu warten
+ leicht zu erweitern
Nachteile:
- Keine Ahnung, was dafür geeignet istedit: Kleine Fehler im Beispielcode bereinigt
-
Vielleicht sowas?
template<typename T> void AnweisungsblockA( std::vector<T>& container ) {...} template<typename T> void AnweisungsblockB( std::vector<T>& container, int arg ) {...} #define EXEC0(F) F(a),F(b),F(c) #define EXEC1(F,z) F(a,z),F(b,z),F(c,z) int main () { std::vector<A> a; std::vector<B> b; std::vector<C> c; EXEC0( AnweisungsblockA ); EXEC1( AnweisungsblockB, 7 );Ist aber ziemlich primitiv..
-
Was mir grad auch noch eingefallen ist, ist aber ebenfalls sehr restriktiv:
#include <boost/preprocessor/repetition/repeat.hpp> // Deine Typen struct ABC0; struct ABC1; struct ABC2; #define CLASS_COUNT 3 #define DECLVECS(z,n,text) std::vector<ABC##n> var##n; #define EXEC(z,n,text) text( var##n ); int main() { BOOST_PP_REPEAT( CLASS_COUNT, DECLVECS, b ); // Evaluiert zu: // std::vector<ABC0> var0; // std::vector<ABC1> var1; // std::vector<ABC2> var2; BOOST_PP_REPEAT( CLASS_COUNT, EXEC, AnweisungsblockA ) // Evaluiert zu: // AnweisungsblockA( var0 ); // AnweisungsblockA( var1 ); // AnweisungsblockA( var2 ); }
-
Vielleicht mit Typlisten?
/*** Klassen die hinzugefügt werden sollen... ***/ class A { public: void TuWas(int a, int b) {cout << "A mit " << a << " und " << b << " aufgerufen" << endl;} }; class B{ public: void TuWas(int a, int b) {cout << "B mit " << a << " und " << b << " aufgerufen" << endl;} }; class C{ public: void TuWas(int a, int b) {cout << "C mit " << a << " und " << b << " aufgerufen" << endl;} }; // So schauen die Paramter der Funktion aus struct Params { int a; int b; }; // Meine Paramter zum übergeben static Params ParamsTable[] = { {1,2}, {2,3}, {4,5} }; // Für welche Typen wird was gemacht typedef Loki::TL::MakeTypelist<A, B, C>::Result MyTypes; // Der Anweisungsblock für den jemweiligen Typ... template <typename T> struct Anweisungsblock { static void TuWas(int a, int b) { T obj; obj.TuWas(a, b); }; }; // Rufe für jeden Anweisungsblock rekursiv auf template <int32_t size, typename ListOfTypes> struct ForEachAnweisungsblock { inline static void DoIt(Params* p) { // Aufruf Anweisungsblock mit Parametern Anweisungsblock<typename Loki::TL::TypeAt<ListOfTypes, size>::Result>::TuWas(p[size].a,p[size].b); // Nächster Block ForEachAnweisungsblock<size-1, ListOfTypes>::DoIt(p); } }; // Die Blocks sind zu Ende template <typename ListOfTypes> struct ForEachAnweisungsblock<-1, ListOfTypes> { inline static void DoIt(Params* p) { } }; int main(void) { //Aufruf aus main... ForEachAnweisungsblock<Loki::TL::Length<MyTypes>::value-1, MyTypes>::DoIt(ParamsTable); return 0; }Wenn du jetzt eine Klasse hinzufügst, musst du nur deine Parameterliste und die Typliste anpassen...
Typlisten findest du im Buch...
Modernes C++ Design | ISBN: 3826613473Die Loki Bibliothek, die Typlisten schon implementiert findest du hier
Gruß
Tobi
-
Die Kosten von virtuellen Funktionen halten sich meist in Grenzen, da nur ein Zeiger dereferenziert wird. Das gilt natuerlich nur wen man Einfachvererbung nutzt. Aber ohne konkreten Code oder Profilerergebnisse laesst sich im einzelnen schwer etwas sagen. Sofern es sich um Standardhardware handelt, sind virtuelle Methoden normalerweise nicht der Flaschenhals. Schaue erstmal, ob du anderswo optimieren kannst.
-
Vielen Dank schonmal an Badestrand, diese Präprozessortricks sind schonmal eine wesentliche Vereinfachung, damit kann ich auf jeden Fall was anfangen.
Tobias Gerg: Ich lese mir das was du geschrieben hast gerade noch durch, das sieht auch sehr gut aus, ich muss es nur erstmal noch durchschauen.