Lange Select-Statements vermeiden?



  • Hallo,

    ich habe folgendes Szenario: Ich empfange über das Netzwerk verschiedene Telegramme. Diese sind unterschiedlich lang und haben nur eine Gemeinsamkeit: Am Anfang des Telegramms steht ein integer als ID. Entsprechend der ID kann ich entscheiden was jetzt weiter mit dem Telegramm passiert.

    Klassischer Fall für ein Select, oder? Nur habe ich knapp hundert verschiedene IDs, sprich das wird ein seitenlanges Select-Statement.

    Das muss doch anders gehen, irgendwie "eleganter" zu lösen sein!
    Ich bin erst auf das Chain-of-responsibility-Pattern gestoßen, das sah auf den ersten Blick ganz nett aus. Aber auf den zweiten Blick nicht mehr, denn genau genommen ist es doch nur ein haufen if-Statements mit dem Overhead von ein paar Funktionsaufrufen.
    Wäre es vielleicht eine Lösung eine Map zu nehmen, mit der ID als Key und jeweils einem entsprechenden Funktionsobjekt? Oder gibt es da ein anderes Pattern das sich schon genau diesem Problem widmet?



  • Es kommt sehr drauf an was du mit den IDs der telegramme machst. Oft kann man ein und den selben code verwenden und nur andere 'eingangsdaten' selektieren, eventuell aus einem Array, mittels der ID.
    Map oder sontige arten von jumptablen sind natuerlich auch moeglich. wobei in dem fall ein select so ziemlich das selbe macht.



  • Was meinst Du den mit SELECT-Statement?



  • vll. VB Select /case? oder SQL SELECT.. wer weis;) 👍



  • Unix-Tom schrieb:

    Was meinst Du den mit SELECT-Statement?

    Hmm, wie komm ich auf Select? o.O
    Ich meinte eigentlich das da - sorry 🙄

    switch (telegram.id)
    {
    case 1:
    	tu_jenes(telegram.rest);
    	break;
    
    case 2:
    	tu_dieses(telegram.rest);
    	break;
    
    // ...
    
    case 999:
    	foo(telegram.rest);
    }
    


  • Hmm, wie komm ich auf Select? o.O

    In VB nennt man das so glaub ich...

    Das ist schwer sowas zu vermeiden, du wilst bei jedem Index eine andere funktion ausführen.. entweder du überlegst dir ein neues design, oder machst es so:)

    Obwohl du könntest dir ein array anlegen, welche funktionpointer alles 1000 funktionen entsprechend dem indes enthält und durchläufst dies???



  • Hi,
    wenn Die ints einigermaßen in einem zusammenhängenden Bereich liegen würde ich mit einem Array arbeiten und aus dem int einen Index bauen.
    Ansonsten, wenns zeitkritsch ist mit binärer Suche arbeiten.
    Gruß Mümmel



  • Nimm doch std::map.
    Die Id ist der Schlüssel, der Value die Funktion. Der Schlüssel kann dann ein int oder etwas komplexeres sein, z.B. ein std::string.



  • Ich hab das jetzt entsprechend euren Ratschlägen mit einer map so gemacht:

    template<typename _key, typename _fn>
    class FnMap
    {
    public:
    	void reg(_key i, _fn f)
    	{
    		fns.insert( std::make_pair(i, f) );
    	}
    
    	_fn operator()(_key i)
    	{
    		return fns[i];
    	}
    private:
    	std::map<_key, _fn > fns;
    };
    
    FnMap<int, boost::function<void (std::string)> > fm;
    fm.reg(1, &foo);
    fm.reg(999, &bar);
    // ...
    while(receive telegram)
    	fm(telegram.id)(telegram.rest);
    

    Das ist schon viel schöner, weil ich an einer Stelle wo es nicht stört alle Funktionen registrieren kann - und die eigentliche Funktion mit dem Empfang usw. bleibt schön sauber.
    Das einzige was mir jetzt noch aufstößt ist dieser hässliche Aufruf mit den zwei Klammern. Ich hätte statt fm(id)(parameter, ...); lieber fm(id, parameter, ...);.
    Ich denke mal das geht bestimmt, aber da ich das ganze auch an anderer Stelle einsetzen möchte, wo es dann mehrere Parameter geben könnte, weiß ich absolut nicht wie. Kann mir da jemand auf die Sprünge helfen?



  • Moin

    was mir auffällt, was passiert wenn du 2 mal die gleiche funktions nummer fersuchst zu registrieren?

    was passiert, wenn für den key kein eintrag existiert?

    ist ggf der Array operator etwas für dich? sieht aber auch blöd aus.

    ein 3. parameter für das Template, das die Parameter art angibt?
    und anstelle des () operators eine Methode execute( <_key>, <_param> )
    Mehrere parameter funktionieren dann nicht als parameter, aber darür gibts strukturen.
    ggf ein macro für die template instanzierung definieren. ( zwgs fehler _fn und _param)

    ggf währe ein default funktion für alle nicht registrerten funktionen sinnvoll.

    gruss



  • Du kannst den operator() mit beliebig vielen Parametern erzeugen (auch zusätzlich zum anderen operator()):

    void operator()(_key i, const std::string &s)
    {
       fns[i](s);
    }
    

    Hier könntest (und solltest) du dann auch noch die Fehlerabfrage einbauen (wie von Termite vorgeschlagen).

    P.S. Wenn du unterschiedliche Maps dieser Art benötigst, solltest du den Parameter "std::string" zum Template hinzufügen (bzw. diesen Typ sowie den Rückgabetyp aus boost::function ermitteln).



  • switch ist im Prinzip schon das richtige, Optimierungen wie Sprungtabelle etc. kann der Compiler auch sehr gut selbst machen (wahrscheinlich sogar besser als du).

    Wie wärs denn mit ein wenig Präprozessor(=Schwarzer) Magie?

    #include <boost/preprocessor/repetition/repeat_from_to.hpp>
    
    #define CASE(z, n, text)                  \
        case n:                               \
            handle_telegram< n > (telegram);  \
            break;
    
    template <int>
    void handle_telegram (telegram_type const&) {}
    
    // Spezialisieren für die jeweiligen Telegrammtypen (definiert durch eine 
    // Ganzzahlkonstante), gerne auch benannt, zB TELEGRAM_*
    // So in etwa:
    const int TELEGRAM_WAR_WAS_BEGINNING = 0x42;
    
    template <>
    void handle_telegram<TELEGRAM_WAR_WAS_BEGINNING> (telegram_type const&)
    {
        std::cout << "A.D. 2101\nwar was beginning!";
    }
    
    // Dann das hier in der Funktion, die die Telegramme verarbeitet:
    switch (telegram.id)
    {
    BOOST_PP_REPEAT_FROM_TO(0, 999, CASE, )
    }
    

    /edit: Hab den Code und die Kommentare etwas geändert. Jetzt klarer, Termite?



  • was mir auffällt, was passiert wenn du 2 mal die gleiche funktions nummer fersuchst zu registrieren?

    Das wird einfach nicht ausgeführt, da könnte ich noch ein assert reinmachen, stimmt. Ist ja wahrscheinlich eine Funktion zweimal reingepackt.

    was passiert, wenn für den key kein eintrag existiert?

    Dann kommt eine exception. Ich bin mir noch nicht ganz sicher ob ich das nicht sogar gut fände - muss ich nochmal drüber nachdenken.

    @.filmor:
    Uiui - Präproz.Magie ist mein Einhorn. Kann der Compiler wirklich große switch-statements optimieren? Wäre wohl anzunehmen, das stimmt... Ich _versuchs_ mal 🙂



  • @.filmor

    Irgendwie komm ich mit dem Template nicht klar.

    zeile 8 und 9 definieren doch ein Funktions template. oder lieg ich da falsch.

    in zeile 5 wird dieses Funktions template aufgerufen. quasi an den tausend stellen im switch case. Wozu die <n> bei einem funktions template?

    wo befindet sich die implementierung der funktionen?

    gruss.



  • n Template-Spezialisierungen, weil du n case Label hast. Du willst ja jeden Fall unterschiedlich behandeln.

    Die Implementierung der Spezielisierungen musst du schon selber machen.

    Die gehören hier hin:

    // Spezialisieren für die jeweiligen Typen, gerne auch mit benannten Konstanten
    // zB TELEGRAM_*
    

    Gruß
    Don06



  • So, ich habe jetzt beide Methoden ausprobiert. Der Code ist wesentlich kürzer als mit ausgeschriebenem switch-case, wobei ich auch bei der Methode mit der Map einiges an Schreibarbeit mit BOOST_PP_Zauberei abgekürzt habe. Was mich überrascht hat ist der doch drastische Geschwindkeitsunterschied: Die Map ist mit gcc3.4.5 und -O3 fast doppelt so schnell (Das Testprogramm hat 200 case-fälle) 😮 Mit einem anderen Compiler (MSVC und gcc4.3) will ich das aber nochmal nachtesten.
    Nur mal Falls es jemanden interessiert.

    Edit: Tja, das kommt dabei raus wenn man nicht lesen kann (was man selbst ausgeben lässt). Es verhält sich genau anders rum, switch ist schneller als map 🙄



  • SIEG!!11einself


Anmelden zum Antworten