Vermeidung von switches



  • Da sich in einem anderen Thread die Frage aufgetan hat, wie sich switches in Spezialfällen vermeiden lassen, würde ich das Thema gern hier diskutieren.

    Beispiel 1: Messageempfang über Socket:
    Szenario: man bekommt einen Buffer aus einem Socket heraus, parst z.B. den Header, erhält dann einen MsgType, eine MsgLänge und noch geparsten oder ungeparsten Content.

    Also mache ich dann sowas hier, um auseinander zu klamüsern, was in welchem Fall zu tun ist:

    void onMessageReceived( MessageClass &msg )
    {
        switch ( msg.msg_type() )
        {
            case MSG_TYPE_START_PROCESS : doThis(); break;
            case MSG_TYPE_END_PROCESS: doThat(); break;
           default : throw ....
        }
    }
    

    Beispiel 2: Object-Factories
    Szenario: ich habe in der Konfigurationdatei eine Liste von Window-Types ( z.B. QMainWindow-Ableitungen in QT ) die dort per ID oder als Text definiert sind.
    Beim Start des Frontends lese ich jetzt die Konfigurationsdatei aus und will die spezialisierten Fenster erzeugen, anhand der Definitionen in der Konfig ( ID oder Text ).

    Aus meiner Sicht geht das nur über eine Art Factory:

    std::shared_ptr<BasisFensterTyp> createObjectByType( int objecttype )
    {
       switch ( objecttype )
       {
           case WINDOWTYPE_1 : return std::make_shared<SpezialisiertesFenster> (... );
           default : return nullptr;
       }
    }
    
    1. Beispiel: InterThreadkommunikation
      Ich habe mehrere Threads und kommuniziere über eine Q, die Instanzen einer bestimmten Basisklasse ( Ableitungen) beherbergt.
      Der Empfangsthread liest diese Q aus, und da steht dann z.B. eine Art MsgType drin, anhand dessen ich erkennen kann in welche Spezialisierung ich die Basis-Klassen-Instanz casten muss:
    void onMessageReceived( std::shared_ptr<BaseClass> &obj )
    {
        switch ( obj->msg_type() )
        {
            case MSG_TYPE_DELETE_OBJECT : deleteObjectFromContainer( std::dynamic_pointer_cast<SpecificClass> ( obj ) ); break;
            case MSG_TYPE_CREATE_OBJECT : createObjectInToContainer( std::dynamic_pointer_cast<SpecificClass> ( obj ) ); break;
            default : throw ....
        }
    }
    

    Gibts Alternativen dazu?



  • Eine mögliche Alternative sind Maps mit Funktionen:

    std::map<MessageType, std::function<void()>> msgFunc;
    
    msgFunc[MSG_TYPE_START_PROCESS] = startProcess;
    msgFunc[MSG_TYPE_END_PROCESS] = endProcess;
    
    auto it = msgFunc.find(msg.msgType);
    if (it != msgFunc.end())
      (it->second)();
    else
      ; // ...
    

    (den Aufruf kann man natürlich noch in eine allg. (Template-)Funktion packen, s. Ideone-Code)

    Die Map kann man auch in eine eigene Klasse packen, welche dann noch Funktionen wie Register, Unregister etc. enthält, um das ganze universeller zu machen.

    Diese Vorgehensweise benutze ich häufig in meinen C# -Projekten (dort heißen die "Funktionszeiger" dann Delegates).



  • So eine Funktionslösung hab ich auch schon vermutet.
    Das funktioniert allerdings nur, wenn die Funktionen quasi alle den gleichen Parameter haben ( z.B. die Basisklasse ), richtig? Denn sonst kriegt man das ja gar nicht alles in eine Map gepresst...

    also quasi ne Art ( auf mein Beispiel bezogen ):

    std::unordered_map<MessageType, std::function<void( std::shared_ptr<BaseClass> )> MessageTypeMap;
    

    Also das mit dem register/unregister gefällt mir ehrlich gesagt sogar ganz gut.

    Bleibt natürlich die Frage nach der Performance:
    Ich hab teilweise Inter-Threadkommunikation im Bereich oberhalb von 100.000 Msgs/sec.
    Da könnte so ne zusätzliche Map schon bissel dran ziehen...
    Im schlimmsten Fall lässt sich das aber mit ganzzahligen MessageTypen auch etwas komplexer mit nem Vector erschlagen. Ist zwar weniger elegant, lässt sich aber in einer gesonderten Klasse gut verstecken.



  • Ehrlich gesagt, finde ich switches aber nachwievor nicht so dramatisch, wenn man die Weiche nur an einer Stelle braucht und somit die Fehlerquote überschaubar ist.



  • Ich denke bei deinem Code an etwas automatisch generiertes, wie protobuf. Das selber zu schreiben ist wohl weniger angenehm...
    Ansonsten kann man aber denke ich je nach dem was du für Rahmenbedingungen hast, schon damit leben.
    Ich benutze auch öfter mal maps mit Funktonen für alles mögliche. Da du eh sowas wie enums hast, könntest du auch über compile time perfect hashing nachdenken.



  • @Mechanics sagte in Vermeidung von switches:

    compile time perfect hashing

    Hast du dazu zufällig einen guten Link, der das bissel erklärt. Der Begriff ist mir ehrlich gesagt komplett neu 😉 Ich hab da bisher nur sowas gefunden
    http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.41.2286&rep=rep1&type=pdf



  • Ich verwende switches wenn überhaupt nur mit enums und da mag ich sehr, dass mich der Compiler (zumindest gcc) warnt, wenn man ein enum vergessen hat (bzw. der häufigere Fall, wenn ein enum hinzugekommen ist).



  • @It0101 sagte in Vermeidung von switches:

    Hast du dazu zufällig einen guten Link, der das bissel erklärt.

    Nichts, was ich selber mal ausprobiert hätte. Es gibt gperf, aber für neuere C++ Versionen gibts sicher auch viele Libs, die das inline machen können. Damit müsstest du den Lookup maximal optimieren können.


Anmelden zum Antworten