Alternativen zu vielen if/else
-
Hallo

Ich hab in meinem Programm eine stelle bei denen ich viele Abfragen in Form von if und else tätige. Das sieht aber meiner Meinung nach übel aus und ich hab mir überlegt ob man das nicht irgendwie besser machen könnte.Da ist mir dann auch eine Idee mit Hilfe von std::map gekommen. Auf der einen Seite die Abfrage als Schlüsselwort und dazu dann ein Pointer auf eine Funktion die in dem entsprechenden Fall ausgeführt werden soll.
Das ist soweit ja kein Problem. Aber(!) die Funktionen haben unterschiedliche Parameter und einige davon sind Methoden von Klassen (die Sache mit "Klassenname::Methode" funktioniert hier anscheinend nicht).Gibt es eine Möglichkeit diese map-Variable so umzuschreiben das man mehrere Typen verwenden kann? (boost::any ??? ). Ich komm jedenfalls nicht drauf. Oder eine ganz andere Möglichkeit?
Ich bin noch relativ C++-Anfänger. Das soll nur zum basteln und Horizont erweitern sein... dennoch schonmal Danle!
-
vieleicht solltest du switch verwenden? ansonsten zeig doch mal code, damit wir nicht rumraten müssen in welchem Kontext du sprichst.
-
Die Abfragebedingung ist ein std::string. Also fällt das mit switch schonmal raus. Gedacht hab ich mir in etwa sowas hier:
std::map<std::string, bool (*)( std::string )> Testmap; Testmap [ std::string ( "LoadMap" ) ] = &LoadMap;Und der Aufruf wäre dann in etwa "Testmap[ Kommando ]("map.xml");". Wobei jetzt Kommando ein string ist und zum Beispiel "LoadMap" drin steht...
-
JDieskau schrieb:
Hallo

Ich hab in meinem Programm eine stelle bei denen ich viele Abfragen in Form von if und else tätige. Das sieht aber meiner Meinung nach übel aus und ich hab mir überlegt ob man das nicht irgendwie besser machen könnte.Die Üblichen Alternativen bei vielen if/else sind aus meiner Sicht switch, std::map sowie Vererbung/Polymorphie (oder Kombinationen dergleichen).
JDieskau schrieb:
Da ist mir dann auch eine Idee mit Hilfe von std::map gekommen. Auf der einen Seite die Abfrage als Schlüsselwort und dazu dann ein Pointer auf eine Funktion die in dem entsprechenden Fall ausgeführt werden soll.
Das ist soweit ja kein Problem. Aber(!) die Funktionen haben unterschiedliche Parameter und einige davon sind Methoden von Klassen (die Sache mit "Klassenname::Methode" funktioniert hier anscheinend nicht).Grundsätzlich lässt sich aber nicht alles Vereinheitlichen, und wenn unterschiedliche Parameter nötig sind ist das eigentlich ein Zeichen das diese Bereiche sich nicht ohne weiteres Kombinieren lassen (Jegliches Konstrukt wie boost:any und Variant sollte nicht die erste Wahl sein).
Zum Thema Funktionen und Methoden gibt es aber tatsächlich eine Möglichkeit der Vereinheitlichung. Grundsätzlich ist ein Motto: Wenn etwas nicht direkt erreichbar ist, hilft meist eine Ebene der Indirektion.
An einen Beispiel:
Nehmen wir einfach mal an wir haben ein Menü das wir möglichst generisch halten wollen. Damit das geht ist der erste Ansatz z.B. eine map in der auf der einen Seite ein interner Name, auf der anderen die dahinter liegende Funktion (im Sinne: etwas das auszuführen ist, nicht zwangsweise eine Funktion) ist.
Wie du ja schon selbst festgestellt hast kann es aber sein das die Funktion tatsächlich eine Methode oder etwas mit Parametern sein kann. Damit lässt sich aber nichts anfangen. Wir müssen es erst einmal Vereinheitlichen (Ich werde jetzt mal möglichkeiten des ausgrauens etc. für das Beispiel weglassen).
// Erster Versuch einer Vereinheitlichung (Alternativ auch über den ()-Operator etc.) class MenueFunktion { public: virtual ~MenueFunktion() {}; virtual void Start() = 0; };Nun haben wir eine rein abstrakte Basisklasse die einfach einen Aufruf Start() besitzt, und könnten unsere Fälle nun davon ableiten. Die Standardfälle sind hierbei einfach. Die eine Klasse könnte einen Funktionszeiger enthalten, die andere könnte eine Templateklasse sein, die einmal eine Instanz (eben das benötigst du zu einer Methode außer dem Methodenzeiger) und eine entsprechende Methode aufnimmt...
Dann wäre es möglich, die Fälle in einer map zusammenzustellen (immer über den Zeiger auf die Basisklasse). Man könnte auch so weit gehen diese Basisklasse über Funktionen zu erweitern die bestimmen ob der Menüpunkt auszugrauen ist etc. Und die Abgeleiten Klassen könnte man mit Sonderfällen ergänzen, die z.B. die entsprechende Instanz erst zum Zeitpunkt des Aufrufes bestimmen (So etwas wie "GetSelectedAddress()" mit anschließenden "addressObj.Edit()")...
Ich kann gerne noch ein Beispiel ausgestallten, habe aktuell aber nicht die Zeit. Themen wie Funktoren sind hier aber ein guter Anhaltspunkt (Ebenso boost::bind, boost::mem_fn...).
cu André
-
Wenn es ultraschnell sein soll kommst du um switch nicht herum (siehe hier). Es lohnt sich vermutlich dann auch, die Strings irgendwie zu hashen oder gar nicht erst Strings sondern Konstanten zu verwenden (was vor allem dann möglich ist, wenn deine Vergleiche sowieso nur aus Literalen bestehen).
-
Wenn du die Funktionen nicht direkt in die Map packen kannst weil sie "zu verschieden" sind, kannst du statt dessen Keys in diese map packen. Ich hatte dazu mal ein Beispiel gebracht, habs grad nochmal rausgekramt und ein klein wenig erweitert:
void processString(std::string const& str) { enum { NoKey = 0, Bla, Blubb, Bla2, Omfg }; //die integralen werte, in die die strings uebersetzt werden static std::map<std::string, int> smap; //einmalig angelegtes "woerterbuch" static struct S{ S(){ //der ctor des Objekts traegt die Werte ins "woerterbuch" ein smap["bla"] = Bla; smap["blubb"] = Blubb; smap["BLUBB"] = Blubb; smap["bla2"] = Bla2; smap["omfg"] = Omfg; }} s; switch(smap[str]) { case NoKey: /* Default-initialisierung von int ist 0, also NoKey. Siehe unten... */ case Bla: /*...*/ case Blubb: /*..*/ //... } }Kurze Erklärung:
smap ist eine statische map, gibts also nur einmal. S ist eine lokale klasse (bzw. struct), von der es ein einziges statisches Objekt gibt. Bei der Konstruktion des Objekts werden die Werte in die Map eingetragen, das geschieht beim ersten Aufruf der Funktion.
Bei jedem Durchlauf der Funktion wird operator[] der map aufgerufen mit dem übergebenen String. Wenn der String nicht vorgesehen ist, dann wird er als key neu in der map abgelegt und der neu erzeugte int ist 0. die zugehörige case-Marke ist dann "NoKey", dort kommt also die Fehlerehandlung hin für falsche strings.
Bei den anderen case-marken kannst du dann die jeweiligen Funktionen, Methoden usw. aufrufen und im Grunde machen was du willst.
-
Danke für eure Antworten. Werde mir das mal alles in Ruhe ansehen und rumexperimentieren. @asc, das muss ich langsam verdauen. So richtig verstanden hab ichs bis jetzt noch nicht. Die Erleuchtung kommt vielleicht noch wenn ich erstmal verstanden hab was dieses virtual ist

Ich also erstmal Lesestoff. Danke

-
JDieskau schrieb:
Die Erleuchtung kommt vielleicht noch wenn ich erstmal verstanden hab was dieses virtual ist

Damit solltest du dich befassen, unabhängig welche Lösung du verwendest, da virtual in der OOP nicht grade unwichtig ist, sobald es um Vererbung geht.
Hier ein kurzes Beispiel was ich anschließend erkläre:
#include <iostream> #include <vector> #include <boost/shared_ptr.hpp> class Base { public: // Bei Basisklasse sollte in der Regel Destruktor immer virtuell sein virtual ~Base() {} // Dies ist eine rein virtuelle Funktion, komme mit der Erklärung später virtual void foo() = 0; }; class A : public Base { public: void foo() { std::cout << "A::foo()\n"; } }; class B : public Base { public: void foo() { std::cout << "B::foo()\n"; } }; int main() { std::vector<boost::shared_ptr<Base> > liste; liste.push_back(boost::shared_ptr<Base>(new A())); liste.push_back(boost::shared_ptr<Base>(new B())); liste.push_back(boost::shared_ptr<Base>(new B())); liste.push_back(boost::shared_ptr<Base>(new A())); for(std::vector<boost::shared_ptr<Base> >::const_iterator pos = liste.begin(), end = liste.end(); pos != end; ++pos) { (*pos)->foo(); } int a; std::cin >> a; }Ein kleines Vorwort, da du vermutlich boost::shared_ptr nicht kennst: Dies ist ein sogenannter Smartpointer, der sich darum kümmert das am Schluß delete aufgerufen wird (er macht noch mehr, aber für das Beispiel reicht es zu wissen das es mir nur darum geht das ich nicht anschließend die Elemente händisch löschen muss). Das Beispiel hätte auch mit einen std::vector<Base*> geklappt, nur müsste man dann wie gesagt an das delete denken...
Nun aber zum Eigentlichen: Ich habe eine Basisklasse Base und zwei davon abgeleitete Klassen A und B. Base besitzt als rein virtuelle Methode "void foo()", die von den abgeleiteten Klassen überschrieben wird. Zugreifen tue ich über Zeiger auf die Basisklasse, sprich der konkrete Typ ist zum Zeitpunkt des aufrufend von foo() dem Anwender unbekannt. Nun kommt der Mechanismus zum tragen der mit virtual zu tun hat. virtual sorgt davor das die "richtige" Methode aufgerufen wird (Um genau zu sein: die Methode im Vererbungsbaum die am nächsten am konkreten Objekt ist, dies ist wichtig falls es über mehr als 2 Ebenen geht).
Nun gibt es zwei möglichkeiten virtual zu verwenden:
a) Virtuelle Methode:
Diese Methode hat eine Definition, sofern in der abgeleiteten Klasse diese Methode nicht überschrieben wird, wird diese ausgeführt (Sprich: dies ist dann sinnvoll wenn du weist wie der Regelfall abzuarbeiten ist, aber auch eine andere Variante zulassen möchtest).
b) Rein Virtuelle Methode:
Diese Methode hat nur eine Deklaration (mit einem = 0 am Ende). Von einer Klasse die rein virtuelle Methoden bestitzen, können keine Instanzen gebildet werden (das gleiche gilt für abgeleitete Klassen, die diese Methoden nicht überschreiben). Dies ist dann sinnvoll wenn die Klasse entweder als reine Schnittstelle dient (entspricht dem Interface in Java) oder wenn es nicht möglich wäre hierfür eine sinnvolle Behandlung zu schreiben, ohne das wissen der abgeleiteten Klassen zu haben.
Ein Sonderfall: Man kann auch den Destruktor rein virtuell deklarieren, muss diesen aber dennoch definieren. Dies ist dann sinnvoll, wenn die Klasse als solche nicht instanzierbar sein soll, aber es keine andere rein virtuelle Methode gibt.cu André