Hallo,
ich schreibe gerade einen "Übersetzer" zwischen zwei Netzen, der mir die
aus dem einen Netz empfangene (binäre) Daten in etwas übersetzt, was ich
in das andere weiter schicken kann (ASCII).
Das erste Byte eines jeden Datenblocks ist eine Zahl als "Kommando".
Dadurch ist automatisch die Länge des danach gesendeten Wertes, die Art
des Wertes, die Funktion zur Umrechnung und die Funktion zum
Zusammenbasteln des übersetzten Strings festgelegt.
Ich könnte also einfach eine Struktur definieren:
1
structcomm{
2
uint8_tlen;
3
uint8_t*val;
4
f_ptrcalculate_val;
5
f_ptroutput_string;
6
};
7
structcommcommand[10];
8
...
Dann könnte ich bei jedem empfangenen cmd_byte einfach prüfen, ob die
Payload danach = command[cmd_byte].len lang ist, mit der Funktion
command[cmd_byte].calculate_val() den Wert berechnen, in *val speichern
und mit der Funktion output_string weitersenden.
Das wäre doch elegant.
Leider sind die cmd_bytes nicht aufeinander folgende Zahlen, sondern 08,
21, 27, ...
Nun kann ich dem natürlich abhelfen mit
1
switch(cmd_byte){
2
case07:
3
cmd_idx=0;
4
break;
5
case21:
6
cmd_idx=1;
7
break;
8
....
9
}
aber das ist irgendwie weit weniger elegant. Dann kann ich auch gleich
die Funktionsaufrufe und Berechnungen noch mit in die case Blöcke
packen, und es wird hässlich zu lesen.
Gibt es eine Alternative?
Case-Switcher schrieb:
void foo(void) {
> switch (cmd_byte) {> case 07:> fkt_07();> break;> case 21:> fkt_21();> break;> }
}//foo
Der Compiler macht daraus in Assembler eine Leiter von Vergleichen und
Sprüngen JMP (nicht CALL), gleich zur fkt_07 (undsoweiter). fkt_07 endet
mit einem RET, was gleich ans Ende der Funktion }//foo führt.
Warum sollte das nicht übersichtlich sein? (wenn man aussagefähige Namen
vergibt)
Erst in einer Tabelle nach dem idx suchen halte ich für
unübersichtlicher, zumal die Tabelle von 08 bis 20 "nichts" enthält.
M.K. B. schrieb:> Eine Lookuptabelle in der man das Kommando sucht und der Index ist dann> der cmd_idx.
Aber nur, wenn man an jedem Byte spart. Sonst macht man halt die
komplette Tabelle und sucht nicht.
abc.def schrieb:> Erst in einer Tabelle nach dem idx suchen halte ich für> unübersichtlicher, zumal die Tabelle von 08 bis 20 "nichts" enthält.
Da sucht man auch nicht, sondern adressiert mit dem Kommandobyte.
Es kommt drauf an, ob Effizienz, Codegröße, Speicherbedarf,
Schreibaufwand beim Programmieren, Lesbarkeit des Quellcodes oder
sonstwas das Kriterium für Eleganz ist.
M.K. B. schrieb:> Eine Lookuptabelle in der man das Kommando sucht und der Index ist dann> der cmd_idx.
Das heißt, die struct wird zu:
1
structcomm{
2
uint8_tcmd;
3
uint8_tlen;
4
uint8_t*val;
5
f_ptrcalculate_val;
6
f_ptroutput_string;
7
};
8
structcommcommand[10];
9
10
// und dann das empfangene cmd_byte zuordnen:
11
12
for(i=0;i<max;i++){
13
if(command[i].cmd==cmd_byte)
14
cmd_idx=i;
15
}
OK. Das sieht etwas hübscher aus.
Macht das einen Unterschied in der Ausführungsgeschwindigkeit oder im
RAM-Verbrauch?
Wo liegen eigentlich die Konstanten der switch... case Anweisung? Im
Flash oder im RAM? Ich nehme an, die kommen nur für die Ausführung des
Codeblocks der switch... case Anweisung ins RAM, oder?
Die struct wäre im RAM. Dann würde 10*sizeof(cmd) zusätzlich verbraucht.
Wolfgang schrieb:> Es kommt drauf an, ob Effizienz, Codegröße, Speicherbedarf,> Schreibaufwand beim Programmieren, Lesbarkeit des Quellcodes oder> sonstwas das Kriterium für Eleganz ist.
In Netz 1 kursieren etwa 100 verschiedene Befehle, von denen mich aber
nur 10 bis 15 interessieren. Bei 15 ist eine komplette Tabelle mit 255
Werten, so dass ich cmd_byte als Index nehmen kann, Verschwendung auf
einem kleinen µC mit 1k RAM und 16k Flash, finde ich.
Ausführungsgeschwindigkeit ist sicher auch ein Kriterium, aber nicht
sooo kritisch. Ich glaube nicht, dass ich eingangsseitig so schnell
einen Überlauf bekomme, weil ich die Daten nicht übersetzt bekomme.
Aber Schreibaufwand/Lesbarkeit wäre mir wichtig. Wenn's unübersichtlich
wird oder wenn zusammengehörende Daten an verschiedenen Stellen
definiert werden, bin ich selbst derjenige, der das pflegen muss.
Welches Schema ein Compiler bei der der Umsetzung von switch Statements
abhängig von den konkreten case Werte implementiert, ist nicht trivial
vorherzusagen, und kann sich bei einer Änderung der Werte oder der
Compilerversion jederzeit ändern.
Sowas finde ich zumindest recht gut lesbar und es lässt sich einfach
erweitern. Setzt allerdings voraus, dass die Payload-Länge nicht allzu
umständlich zu berechnen ist.
Der Compiler wird das schon halbwegs effizient umsetzen, abhängig von
den Parametern (wenn du auf Größe optimierst, eher nicht mit einer
vollen 256 Byte-Tabelle).
Oder du implementierst die gesamte Verarbeitung direkt so:
1
voidhandle_cmd(uint8_t*buf,uint8_tlen){
2
switch(buf[0]){
3
case0x07:handle_first_cmd(buf,len);break;
4
case0x28:handle_second_cmd(buf,len);break;
5
default:panic("unknown cmd %d",buf[0]);break;
6
}
7
}
Das lässt sich ebenfalls wunderbar erweitern und ergibt eine
übersichtliche Struktur im Code. Ob das sinnvoll ist, hängt vom
Befehlssatz ab, im schlimmsten Fall kriegst du viel Boilerplate und
Redundanz rein.
Wenn du den Code aber aus einer anderen Beschreibung (z.B. Excel, XML
oder sowas) generieren lässt, ist das aus meiner Sicht die Struktur der
Wahl.
Case-Switcher schrieb:> ich schreibe gerade einen "Übersetzer" zwischen zwei Netzen, der mir die> aus dem einen Netz empfangene (binäre) Daten in etwas übersetzt, was ich> in das andere weiter schicken kann (ASCII).> Das erste Byte eines jeden Datenblocks ist eine Zahl als "Kommando".> Dadurch ist automatisch die Länge des danach gesendeten Wertes, die Art> des Wertes, die Funktion zur Umrechnung und die Funktion zum> Zusammenbasteln des übersetzten Strings festgelegt.>> Ich könnte also einfach eine Struktur definieren:>
1
structcomm{
2
>uint8_tlen;
3
>uint8_t*val;
4
>f_ptrcalculate_val;
5
>f_ptroutput_string;
6
>};
7
>
...
> Leider sind die cmd_bytes nicht aufeinander folgende Zahlen, sondern 08,> 21, 27, ...>> Nun kann ich dem natürlich abhelfen mit>
1
switch(cmd_byte){
2
>case07:
3
>cmd_idx=0;
4
>break;
5
>case21:
6
>cmd_idx=1;
7
>break;
8
>....
9
>}
10
>
> aber das ist irgendwie weit weniger elegant.
Finde ich nicht. Im Prinzip hast du die zwei Möglichkeiten der Lösung
erkannt. Du kannst die Logik entweder als Programmcode oder als
Datenstruktur aufschreiben.
Programmcode läuft auf ein switch() Statement hinaus. Und ja, dann würde
man die notwendigen Aktionen direkt in den case: Blöcken unterbringen.
Wobei man identischen Code natürlich trotzdem in Funktionen auslagern
und von dort aus aufrufen kann.
Die Alternative besteht aus einem Array (oder einer Liste oder einer C++
std::map) von structs, die notwendige Daten und Funktionszeiger
enthalten. In dem Fall ist der Code selber eher trivial - anhand des
ersten Bytes deiner Nachricht die richtige struct finden und dann die
Funktion per Zeiger aufrufen. Wenn deine Indizes nichtkontinuierlich
sind, ist ein assoziatives Array (z.B. std::map) der kanonische
Container. Eine Liste müßtest du durchsuchen. Ein Array mit dem
Kommandobyte als Index würde Platz verschwenden.
Von der Nachvollziehbarkeit halte ich die switch() Variante für schöner.
Denn im zweiten Fall müßte man immer erstmal die Datenstruktur
konsultieren, um den Namen der referenzierten Action-Funktion zu
erfahren. Das kann man zwar vereinfachen, z.B. indem man sprechende
Namen für die Funktionen verwendet. Aber irgendwann wird sich irgendwer
nicht mehr daran halten and dann ist Essig.
Bei der Variante mit switch() kann man diesbezüglich nichts falsch
machen. Man kann da sogar noch eine Optimierung anbringen, indem man die
häufigsten Fälle an den Anfang zieht.
Erst einmal vielen Dank an alle!
Die "eierlegende Wollmilchsau" gibt's also mal wieder nicht.
Ich kann entweder mit dem array of struct arbeiten und muss dann das
empfangene cmd_byte einem Index cmd_idx zuordnen, entweder durch
1
for(i=0;i<max;i++){
2
if(command[i].cmd==cmd_byte)
3
cmd_idx=i;
4
}
oder durch
1
switch(cmd_byte){
2
case07:
3
cmd_idx=0;
4
break;
5
case21:
6
cmd_idx=1;
7
break;
8
....
9
}
Oder ich packe wie S. R. (svenska) gleich (inline) Wrapper-Funktionen in
das switch() statement.
Axel S. schrieb:> Eine Liste müßtest du durchsuchen. Ein Array mit dem> Kommandobyte als Index würde Platz verschwenden.
Ein array of struct wie oben würde hier sogar das RAM sprengen (1kb).
Also bleibt mit einer for loop zu suchen oder eben switch().
Ich finde die struct eigentlich ganz praktisch, da man da alle
Informationen an einem Ort im Überblick hat.
Schade, dass man in den Array eben nicht direkt hinein springen kann.
Für ein assoziatives Array müsste ich plain C verlassen...
> Oder ich packe wie S. R. (svenska) gleich (inline) Wrapper-Funktionen in> das switch() statement
Die Darstellung ist aber einfach Müll, egal wer da irgendwelche
Kodierungsrichtlienen erstellt. Sowas gehört so, und nicht anders:
1
switch(cmd_byte){
2
case0x07:cmd_idx=0;break;
3
case0x21:cmd_idx=1;break;
4
....
5
}
Dann ist es so klar les- und änderbar wie jede Tabelle
Case-Switcher schrieb:> Ich finde die struct eigentlich ganz praktisch, da man da alle> Informationen an einem Ort im Überblick hat.
Außer den Code selber. Denn von den Funktionen sieht man in der struct
nur den Namen. Eben deswegen finde ich die Variante mit switch()
besser. Der Code steht in unmittelbarer Nähe des Tokens (der Wert nach
dem case: Schlüsselwort).
Wenn man die Funktionsweise verstehen will, ist es trivial im Editor
nach dem Token zu suchen und dann an praktisch der gleichen Stelle den
zugehörigen Code zu finden.