Ich habe eine c-Funktion, in der verschiedene Alternativen von
Berechnungsmethoden getestet werden sollen. Die Berechnungen sind als
c-Funktion implementiert und geben bei Erfolg einen Wert != 0 zurück.
Bei Nichterfolg soll die nächste Methode probiert werden. usw.
Bisher ist das ganze so programmiert:
1
unsignedintcheck()
2
{
3
unsignedintresult;
4
5
result=func1();
6
if(result!=0)
7
returnresult;
8
9
result=func2();
10
if(result!=0)
11
returnresult;
12
13
result=func3();
14
if(result!=0)
15
returnresult;
16
17
...// viele weitere
18
19
return0;//keine Methode erfolgreich
20
}
Nun gelten mehrfache returns bekanntlich als schlechter Programmierstil.
Wie kann man das vermeiden, ohne an Übersichtlichkeit zu verlieren? Eine
Unzahl von verschachtelten if/else Abfragen mit entsprechend vielen
geschweiften Klammern ist meiner Meinung nach keine Alternative.
Mike schrieb:> Nun gelten mehrfache returns bekanntlich als schlechter Programmierstil.> Wie kann man das vermeiden, ohne an Übersichtlichkeit zu verlieren? Eine
Vielleicht auf Null abfragen, anstatt auf != 0.
Mike schrieb:> Nun gelten mehrfache returns bekanntlich als schlechter Programmierstil.
Das ist sicher keine allgemeingültige Meinung.
Mike schrieb:> Wie kann man das vermeiden, ohne an Übersichtlichkeit zu verlieren?
Schwierig, viel übersichtlicher wird es nicht werden, und das ist eines
der wichtigen Ziele in der Programmierung.
Mike schrieb:> Eine> Unzahl von verschachtelten if/else Abfragen mit entsprechend vielen> geschweiften Klammern ist meiner Meinung nach keine Alternative.
Du könntest über die Möglichkeiten loopen.
Ein array von funktionspointern übergeben und durch das array mit einer
for schleife. Dann gibt es nur einen ausstiegspunkt (mit break oder auch
mit return)
Mike schrieb:> Eine> Unzahl von verschachtelten if/else Abfragen mit entsprechend vielen> geschweiften Klammern ist meiner Meinung nach keine Alternative.
Es gibt ja auch noch Goto.
Oder eine Verschachtelung des trinären Operators.
Grundsätzlich gilt ja wohl:
Wenn du die Verzweigungen brauchst, dann brauchst du sie.
Wenn dir multiple Return nicht schmecken, musst du anders dahin
verzweigen.
Es bieten sich da else oder Goto an.
Dumdi D. schrieb:> array von funktionspointern
Wenn es besser "schmeckt", als mehrfache return, dann ja.
Mike schrieb:> Nun gelten mehrfache returns bekanntlich als schlechter Programmierstil.
Was immer wieder Gegenstand von Diskussionen ist. So allgemeingültig
finde ich ist das nicht. Wie Du selbst erkannt hast, endet es sonst in
einem Wust an if-else if-else. Maximale Verschachtelungstiefe wird von
diversen Coding-Standards auch beschränkt. Goto ist auch pöhse.
"Lösung": Unterfunktionen, damit werden alle Metriken erfüllt. Ist doch
egal, dass das nur noch dämlicher wird, weil das eigentliche Problem das
Du zu lösen hast nun mal so ist wie es ist.
Allerdings sehe ich die Annahme "Nun gelten mehrfache returns
bekanntlich als schlechter Programmierstil" als durchaus zwiespältig an.
Bestimmte Konstrukte sind mit mehrfachen Returns klar, kurz und
übersichtlich.
Manche Konstrukte werden durch krampfhaftes Vermeiden mehrfacher Returns
sogar ausgesprochen unübersichtlich.
Arduino Fanboy D. schrieb:> Dumdi D. schrieb:>> array von funktionspointern> Wenn es besser "schmeckt", als mehrfache return, dann ja.
Ernsthaft? Oder sollte das witzig sein?
In einer Welt in der der geneigte Programmierer zu doof ist, mehrere
returns zu verkraften bewerfen wir ihn mit Funktionspointern um ein
dogmatisches Befolgen von allgemeinen "Weisheiten" zu erreichen? Deine
Kollegen oder Nachfolger werden es danken.
Vorteile:
- Keine verschachtelten if-Statements
- Die Bedingungen stehen übersichtlich untereinander
- Die Reihenfolge der func()-Aurufe ist wie gewünscht
- Abbruch der func()-Aufrufe, sobald ein Fehler zurückkommt
- Keine Klammer-Orgie
- Nur ein return
Warnung: alles ungetestet.
Haben die Funktionen alle das gleiche Interface? Dann könntest du
eventuell eine Schleife nehmen:
1
typedefunsignedint(*check_function)(void);
2
3
constcheck_functioncheck_list[]={
4
check_1,
5
check_2,
6
check_3,
7
0
8
};
9
10
unsignedintcheck(void){
11
unsignedintresult=0;
12
for(check_function*f=check_list;*f&&!result;f++)
13
result=f();
14
returnresult;
15
}
Hat der Rückgabewert irgendeine Bedeutung abgesehen von OK/Weiter?
Spielt die Reihenfolge eine rolle? Falls nein, könnte man die Check
Funktionen sich eventuell sogar gleich selbst registrieren lassen:
check_list.h
Mike schrieb:> Nun gelten mehrfache returns bekanntlich als schlechter Programmierstil.
Nö. Les mal Clean Code von Robert C. Martin.
Ist das C-Code oder C++? Im letzteren Fall
kann ich mir eine Lösung mit dem Decorator
Pattern vorstellen.
merciless
Mike schrieb:> Nun gelten mehrfache returns bekanntlich als schlechter Programmierstil.> Wie kann man das vermeiden,
Lass es so. Diese Regel wirkt sich nur aus wenn die unterschiedlichen
returns irgendwo kreuz und quer in verschachtelten Schleifen stecken und
es mühsam wird zu erkennen ob und wann der Teil darunter nun ausgeführt
wird weil man sie übersehen kann oder weil sie an Bedingungen geknüpft
sind für die man sich erstmal das Hirn verrenken muss um sie im Kopf
durchzugehen.
Bei Deinem Beispiel sieht man auf den ersten Blick was da vor sich geht,
da ist nichts versteckt oder überraschend, das vorzeitige Return ist das
Kernthema und der Dreh- und Angelpunkt dieser Funktion, es springt einem
sofort ins Auge und man kann es nicht übersehen. Also lass es so.
Frank M. schrieb:> unsigned int check()> {> unsigned int result;>> if ((result = func1()) == 0 &&> (result = func2()) == 0 &&> (result = func3()) == 0)> {> result = do_something ();> }> return result;> }>> Vorteile:> - Keine verschachtelten if-Statements> - Die Bedingungen stehen übersichtlich untereinander> - Die Reihenfolge der func()-Aurufe ist wie gewünscht> - Abbruch der func()-Aufrufe, sobald ein Fehler zurückkommt> - Keine Klammer-Orgie> - Nur ein return
Ich finde es fürchterlich, weil ich eine Zuweisung innerhalb einer
Abfrage für sowas wie das Ausnützen eines Seiteneffekts halte. Beim
flüchtigen Schnell-drüber-lesen ist die Zuweisung ratzfatz überlesen.
Schlimmer noch: ein Nachfolgeprogrammierer könnte die Zuweisung sogar
für einen Flüchtigkeitsfehler halten und ihn "korrigieren" wollen.
Hirnknötchen völlig ohne Not, da wären multiple returns ganz sicher
vorzuziehen.
Normalerweise quittieren viele Compiler Zuweisungen innerhalb einer
Condition direkt mit einer Warnung.
Thomas M. schrieb:> Ich finde es fürchterlich, weil ich eine Zuweisung innerhalb einer> Abfrage für sowas wie das Ausnützen eines Seiteneffekts halte.
Darüber kann man geteilter Meinung sein.
> Beim> flüchtigen Schnell-drüber-lesen ist die Zuweisung ratzfatz überlesen.
Dann machst du was falsch.
> Schlimmer noch: ein Nachfolgeprogrammierer könnte die Zuweisung sogar> für einen Flüchtigkeitsfehler halten und ihn "korrigieren" wollen.
Dann ist es kein C-Programmierer.
> Hirnknötchen völlig ohne Not, da wären multiple returns ganz sicher> vorzuziehen.
Die Reihe der normalen If() ist die saubere Variante, auch für
empfindliche Gemüter.
Beitrag "Re: c: Wie multiple return-Statements vermeiden?"> Normalerweise quittieren viele Compiler Zuweisungen innerhalb einer> Condition direkt mit einer Warnung.
Dann sind es aber keine C-Compiler.
DPA schrieb:> Warnung: alles ungetestet.>> Haben die Funktionen alle das gleiche Interface? Dann könntest du> eventuell eine Schleife nehmen:>
1
wilderCodeWust
Das ist bestimmt eine nette Programmiersportaufgabe. Aber alleine dass
ich beim Eingangspost mit einem Blick erkannte, was er wollte und diesen
Codewust direkt als tldr; abtat, sagt aus, dass diese Lösung unterlegen
ist.
P.s.: tldr; = to long, didn't read
Wichtig ist, dass der Quelltext gut lesbar ist. Und das ist beim Code im
Eröffnungspost der Fall.
Ich würde hier vielleicht noch ein paar Zeilenumbrüche heraus nehmen.
Es gib immer wieder mal Fälle, wo man von den allgemeinen Stil-Vorgaben
abweichen sollte, um die Lesbarkeit zu verbessern. Man muss nur einen
nachvollziehbaren Grund nennen können, dann ist das meiner Meinung nach
schon Ok.
Wenn du später mal im Team programmierst, wirst du schnell merken, dass
Diskussionen um solche kleinen Details reine Zeitverschwendung sind. Wir
(in der Firma) diskutieren nur über unverständlichen und fehlerhaften
Code.
Thomas M. schrieb:> Ich finde es fürchterlich, weil ich eine Zuweisung innerhalb einer> Abfrage für sowas wie das Ausnützen eines Seiteneffekts halte.
Das ist ganz normales C. Wenn ich mich recht erinnere, lernt man das bei
K&R direkt in Kapitel 1:
1
while((c=getchar())!=EOF)
2
{
3
putchar(c);
4
}
Wenn das schon die grauen Hirnzellen überfordert, sollte man es mit C
besser sein lassen.
> Normalerweise quittieren viele Compiler Zuweisungen innerhalb einer> Condition direkt mit einer Warnung.
Ja, wenn Du die Klammern um die Zuweisung vergisst - und das zu recht.
Die habe ich aber nicht vergessen. ;-)
Falk B. schrieb:>> Normalerweise quittieren viele Compiler Zuweisungen innerhalb einer>> Condition direkt mit einer Warnung.>> Dann sind es aber keine C-Compiler.
gcc und clang können das. In dem beispiel aber nicht. Die zweite () ist
wie man bei den Compilern die Warnung wieder abstellt, und anzeigt, ja,
dass soll ne Zuweisung sein.
Thomas M. schrieb:> Das ist bestimmt eine nette Programmiersportaufgabe. Aber alleine dass> ich beim Eingangspost mit einem Blick erkannte, was er wollte und diesen> Codewust direkt als tldr; abtat, sagt aus, dass diese Lösung unterlegen> ist.>> P.s.: tldr; = to long, didn't read
Das erste beispiel ist nur 15 Zeilen lang, also weniger als das
Original. Weniger Zeichen waren es auch noch. Nur die 2te Option war
etwas länger, aber auch nur weil diese die func1, func2, etc. auch noch
zeigten.
Arduino Fanboy D. schrieb:> Es gibt ja auch noch Goto.
Naja - wer schon multiple returns verpönt, was sagt der wohl zum Thema
goto?
In diesem Kontext (ohne weitere Berechnungen vor dem returm am
Funktionsende) wäre goto return ja effektiv das selbe wie return, nur
halt mit anderen Buchstaben geschrieben geschrieben und der werte Leser
müsste noch Mausrad und Hirn beanspruchen, um das rauszukriegen.
DPA schrieb:> Falk B. schrieb:>> Dann sind es aber keine C-Compiler.>> gcc und clang können das.
Du hast Falk missverstanden.
Er meinte: Wenn ein C-Compiler das anmeckert, was ich anständig in
1
(result=func1())==0
geklammert habe, dann ist es kein C-Compiler. Und da hat Falk vollkommen
recht.
Thomas M. schrieb das nämlich zu allgemeingültig. Er behauptete, dass
einige C-Compiler immer bei Zuweisungen innerhalb eines Vergleichs
meckern. Das stimmt aber nicht. Klammert man sorgfältig, ist das
vollkommen okay und kein C-Compiler der Welt meckert das an.
DPA schrieb:> Das erste beispiel ist nur 15 Zeilen lang, also weniger als das> Original.
1
typedefunsignedint(*check_function)(void);
2
3
constcheck_functioncheck_list[]={
4
check_1,
5
check_2,
6
check_3,
7
0
8
};
9
10
unsignedintcheck(void){
11
unsignedintresult=0;
12
for(check_function*f=check_list;*f&&!result;f++)
13
result=f();
14
returnresult;
15
}
Ehrlich gesagt ... ab einer ausreichend großen Anzahl von check_n
Funktionen würde ich es wohl ziemlich genau so machen. :)
Bei n <= 3 würde ich es jedoch ausgerollt lassen.
Rufus Τ. F. schrieb:> Manche Konstrukte werden durch krampfhaftes Vermeiden mehrfacher Returns> sogar ausgesprochen unübersichtlich.
Haste grade MISRA gesagt?
Mw E. schrieb:> Haste grade MISRA gesagt?
Die Kernaussage, warum man vorzeitige Returns in der Regel vermeiden
sollte:
Werden am Ende der Funktion noch Aufräumaktionen getätigt, läuft man
Gefahr, diese bei vorzeitigen Returns einfach zu vergessen.
Eine Folge könnten zum Beispiel die allseits bekannten Memory-Leaks
sein. Existiert jedoch keine solche Aufräumaktion, habe ich auch keine
Skrupel, ein vorzeitiges Return einzubauen.
Johann J. schrieb:> ... // viele weitere
So ab 70 wirds mit den Einrückungen aber etwas unschön ;)
Ich finde das Original ja nach wie vor gut, aber bitte, einen hab ich
auch noch:
Frank M. schrieb:> Thomas M. schrieb das nämlich zu allgemeingültig. Er behauptete, dass> einige C-Compiler immer bei Zuweisungen innerhalb eines Vergleichs> meckern. Das stimmt aber nicht. Klammert man sorgfältig, ist das> vollkommen okay und kein C-Compiler der Welt meckert das an.
Das mag sein. Aber das ist dann doch auch schon wieder irgendein
Expertenklugsch***erwissen, von dem ich wetten würde, dass, wenn man mit
einer Umfrage - selbst routiniertere und gute - Programmierer damit
konfrontieren würde, es sich herausstellen würde, dass sie es auch nicht
wussten.
Guter Stoff für eine Klausurfrage, aber im täglichen Einsatz so nutzlos
wie Schachtelsätze.
Frank M. schrieb:> Werden am Ende der Funktion noch Aufräumaktionen getätigt, läuft man> Gefahr, diese bei vorzeitigen Returns einfach zu vergessen.
Genau. Und diesen Zustand umschifft man stilgerecht mit goto anstelle
von return ;-)
Oder man baut ne ne dummy-Schleife und verlässt sie mit break.
Frank M. schrieb:> Werden am Ende der Funktion noch Aufräumaktionen getätigt, läuft man> Gefahr, diese bei vorzeitigen Returns einfach zu vergessen.
Dafür wurde in Java try-finally erfunden. Leider hat C/C++ das nicht.
Thomas M. schrieb:> Das mag sein.
Das ist so. Zumindest sollte man den Basis-C-Standard verstanden
haben.
> Aber das ist dann doch auch schon wieder irgendein> Expertenklugsch***erwissen,
Wenn schon die C-Bibel von Kernighan & Ritchie als zweites
Programmierbeispiel
1
while((c=getchar())!=EOF)
direkt nach dem Hello-World-Programm aufführt, dann sind das
Anfänger-Basics und keinesfalls "Expertenklugsch***erwissen", wie Du
es ausdrückt.
> Guter Stoff für eine Klausurfrage, aber im täglichen Einsatz so nutzlos> wie Schachtelsätze.
Das ist im täglichen Einsatz. Und Du wirst diese Zuweisungen auch in
fast jedem Open-Source finden, die etwas größer sind als Dreizeiler.
Frank M. schrieb:> if ((result = func1()) == 0 &&> (result = func2()) == 0 &&> (result = func3()) == 0)> {
&& ?
ich weiss nicht, was all die function pointer orgien hier sollen ...
simple und kurz gehts auch!
uint_t check() {
uint_t result;
if (result=func1() || result=func2() || result=func2()) return result;
return 0;
}
mt
Frank M. schrieb:> Zunächst siehst Du da die goto-Orgie, wie W.S. das umgesetzt hat
Hmm, wer hat da wohl von wem abgeguckt. Sieht ja fast so aus, wie mein
Beispiel.
Frank, dein Beispiel sieht schöner aus. Aber wenn die Blöcke etwas
komplexer werden, wie in meinem obigen i2c Beispiel, dann klappt das
nicht mehr mit der besseren Lesbarkeit.
Goto ist allgemein zu vermeiden, und mehrere Returns sollte man sich
auch gut überlegen. Manchmal machen sie aber Sinn.
Oliver S. schrieb:> So ab 70 wirds mit den Einrückungen aber etwas unschön ;)
Noch nie was von "ordentlichen Bildschirmgrößen" gehört? :-)
Bei solchen Schachtelungstiefen mit 7465 Checks würde ich dann eher so
vorgehen (dann kann man check() auch gleich weglassen):
Stefanus F. schrieb:> Ich glaube das ist das einzige Szenario, in dem ich außerhalb des C64> jemals goto verwendet habe.
Mit Goto kann man schöne und einfache endliche Automaten bauen.
Switch/case ist da nicht unbedingt so effektiv.
---
Bei diesem Problem hier ist es echt egal, ob man per goto zu einem
zentralen Ausstieg hüpft, den Code mit Dutzenden Return pflastert, oder
eine if/else Kaskade aufbaut.
Einen Schönheitswettbewerb wird man damit sowieso nicht gewinnen
Rechteckig, hat er zu sein, der Code.
Symmetrisch soll er sein. (Kunst für Doofe)
Stefanus F. schrieb:> Hmm, wer hat da wohl von wem abgeguckt. Sieht ja fast so aus, wie mein> Beispiel.
Ich weiß nicht, was Du jetzt andeuten willst.
Ich habe lediglich den Code von W.S. konkret in dem genannten Beitrag
rausgepickt und ad hoc umgeschrieben. Selbst verwende ich weder diesen
Code noch Deinen. Mein I2C-Code, den ich für STM32 verwende und unter
GPL veröffentlicht ist, sieht gänzlich anders aus.
Stefanus F. schrieb:> Ich glaube das ist das einzige Szenario,
und das goto zur fehlerbehandlung ist im professionellen umfeld häufig
bis standard. also goto ist kein teufelszeug!
es gibt noch viele andere "tricks" z.b. die verwendung von "!!"
(negation der negation).
schon mal gesehen und den sinn in c verstanden?
mt
Apollo M. schrieb:> es gibt noch viele andere "tricks" z.b. die verwendung von "!!"> (negation der negation).
Sollte den aufmerksamen Lesern von µC.net allseits bekannt sein. Peter
D. verwendet das sehr gern, um Werte auf 0 und 1 zu "normieren".
Arduino Fanboy D. schrieb:> Switch/case ist da nicht unbedingt so effektiv.
wie leicht man doch getäuscht wird ...
switch/case ist nichts anderes als goto in einem anderen gewand
mt
Frank M. schrieb:>> Hmm, wer hat da wohl von wem abgeguckt. Sieht ja fast so aus, wie mein>> Beispiel.>> Ich weiß nicht, was Du jetzt andeuten willst.
Ich wollte damit andeuten, dass die Ähnlichkeit zwischen W.S Code und
meinem verblüffend hoch ist. Was für ein bemerkenswerter Zufall!
wolpino schrieb:> Mit dem guten alten do-while(0) Trick kann man es so machen:
Woraus man lernt, dass ein "break" auch nichts anderes als ein goto ist
;-)
Ja, man sollte goto generell nicht verteufeln. Aber man muss es nicht so
extensiv wie ein Old-School-Basic-Programmierer einsetzen.
Frank M. schrieb:> Sollte den aufmerksamen Lesern von µC.net allseits bekannt sein. Peter> D. verwendet das sehr gern, um Werte auf 0 und 1 zu "normieren".
korrekt! ich bin dann wohl ein spätzünder, weil erst spät realisiert.
ich denke, wir könnten mal breiter über clevere implementation tricks
reden ... das würde sich lohnen!
mt
wolpino schrieb:> Mit dem guten alten do-while(0) Trick kann man es so machen:
Äh, das gefällt mir nicht. Wenn ich do/while sehe erwarte ich eine
Wiederholschleife.
Thomas M. schrieb:> Frank M. schrieb:>> unsigned int check()>> {>> unsigned int result;>>>> if ((result = func1()) == 0 &&>> (result = func2()) == 0 &&>> (result = func3()) == 0)>> {>> result = do_something ();>> }>> return result;>> }>>>> Vorteile:>> - Keine verschachtelten if-Statements>> - Die Bedingungen stehen übersichtlich untereinander>> - Die Reihenfolge der func()-Aurufe ist wie gewünscht>> - Abbruch der func()-Aufrufe, sobald ein Fehler zurückkommt>> - Keine Klammer-Orgie>> - Nur ein return>> Ich finde es fürchterlich, weil ich eine Zuweisung innerhalb einer> Abfrage für sowas wie das Ausnützen eines Seiteneffekts halte. Beim
Solche Konstrukte sind eher Sache der Gewohnheit. Bei den meisten
Programmen sind Abfragen sehr einfach und der bedingte Code
komplizierter. Bei komplexen Aufgaben kann aber auch oft der umgekehrte
Fall auftreten, sehr komplexe Bedingungen und simple (oder komplexe)
Aktion. Oft sind von der Bedingung Werte einzusammeln, ähnlich wie beim
Pattern-Matching, wo, wenn ein Pattern passt, dieses eingesammelt und
dann bedingt fortgefahren wird (unter verwendung des gematchten Texts).
> flüchtigen Schnell-drüber-lesen ist die Zuweisung ratzfatz überlesen.
Ist wie gesagt Gewohnheit, auch aufmerksames Lesen bzw. sinnvolle
Commentare zu schreiben... Und man kann das Konstrukt auch außerhal
einer Bedinung verwenden:
1
(void)
2
(result=func1()
3
||result=func2()
4
||result=func3()
5
...
6
||result=funcN());
7
8
if(result)
9
returnresult;
Hier wird Shortcut-Evaluation benutzt, d.h. die Auswertung des ||
Ausdrucks erfolgt von links nach rechts und wird beendet, sobald result
!= 0 ist.
> Normalerweise quittieren viele Compiler Zuweisungen innerhalb einer> Condition direkt mit einer Warnung.
GCC gibt eine Warnung, allerdings nicht wenn die Zuweisung in Klammern
steht, weshalb Frank die Zuweisungen klammerte.
Johann L. schrieb:> Shortcut-Evaluation
... ist sehr nützlich und mächtig, NUR scheuen die hobby/basic jünger
dieses konstrukt wie das feuer genau so wie z.b.
switch(c=f())
witch(c=f() && ..)
if(c=f() && b=f2() || ...)
hier mal ein beispiel aus der macro ecke zu short-eval alias k-logic
//creates an unique timer for each instance, x: repeats 1-255
#define XPERIODIC(tb, dt, t, x) \
static dt UNIQUE(tmr) = (dt) tb; \
static uint8_t UNIQUE(xRepeats) = x; \
if ((tb - UNIQUE(tmr) >= t) \
&& UNIQUE(xRepeats) \
&& (UNIQUE(xRepeats)--, UNIQUE(tmr) = tb, 1)) //UNIQUE(tmr) += t, 1))
mt
Ja im Falle dass man Aufräumaktionen machen muss würde ich sogar ein
goto befürworten.
Sprachfeatures sind in aller Regel nicht an sich gut oder böse, es liegt
am Programmierer sie sinnvoll und mit Bedacht anzuwenden.
Auch so was wie der "Destruktive Zuweisungsoperator" (= in C) kann für
absolut furchtbaren Code sorgen. (Deshalb haben den viele Sprachen auch
gar nicht drin)
Apollo M. schrieb:> if(c=f() && b=f2() || ...)
Da garantiert Dir übrigens keiner in welcher Reihenfolge die Funktionen
ausgeführt werden. Manche Programmierer nehmen da einfach zum Beispiel
an, dass die von rechts nach links abgearbeitet wird, was aber nicht
immer der Fall ist.
Das Ergebnis von sowas sind dann "Heisenbugs". Fehler die im Livesystem
auftreten, im Debuger jedoch nicht, sowie Code der abhängig von der
Optimierungsstufe unterschiedliche Dinge tut.
Christian B. schrieb:> Da garantiert Dir übrigens keiner in welcher Reihenfolge
darum gilt es dann diese variante zu verwenden
if(c=f(), c && b=f2(), b || ...)
dann sollte es passen!?
mt
Stefanus F. schrieb:> wolpino schrieb:>> Mit dem guten alten do-while(0) Trick kann man es so machen:>> Äh, das gefällt mir nicht. Wenn ich do/while sehe erwarte ich eine> Wiederholschleife.
...deren Abbruchbedingung im while klar und deutlich zu sehen ist.
Christian B. schrieb:> Apollo M. schrieb:>> if(c=f() && b=f2() || ...)>> Da garantiert Dir übrigens keiner in welcher Reihenfolge die Funktionen> ausgeführt werden. Manche Programmierer nehmen da einfach zum Beispiel> an, dass die von rechts nach links abgearbeitet wird, was aber nicht> immer der Fall ist.
Nö, && und || garantieren Links-nach-Rechts Auswertung ihrer Operanden
mit Sequenzpunkten.
Apollo M. schrieb:> Arduino Fanboy D. schrieb:>> Switch/case ist da nicht unbedingt so effektiv.>> wie leicht man doch getäuscht wird ...>> switch/case ist nichts anderes als goto in einem anderen gewand>> mt
Du hast mich vermutlich nicht richtig verstanden, macht aber auch nix,
weil switch/case hier sowieso nicht weiter hilft.
Jemand schrieb:> && und || garantieren Links-nach-Rechts Auswertung ihrer Operanden> mit Sequenzpunkten.
so ist es! die zweite variante kann vielleicht die lesbarkeit verbessern
und sonst nichts.
Schade, dass in C die Ergebnisse der && und ||-Operationen auf 0 und 1
beschränkt sind. In Python, wo dies nicht der Fall ist, kann man bspw.
folgendes schreiben:
Johann L. schrieb:> (void)> (result = func1()> || result = func2()> || result = func3()> ...> || result = funcN());>> if (result)> return result;
Das ist falsch, wegen Operator Precedence (result = func1() || result
...)
Der Compiler (gcc) moniert:
1
if.c:14:16: error: lvalue required as left operand of assignment
leo schrieb:> Das ist falsch, wegen Operator Precedence (result = func1() || result> ...)
klammern, dann geht es!
if ((result = func1()) || (result = func2()) || (result = func3()))
...
mt
Das Beispiel selbst is eigentlich komplett funktional und schreit
förmlich Monade. Das is in C halt leider eher häßlich und "short
curcuit"-en tuts auch nicht.
>> Äh, das gefällt mir nicht. Wenn ich do/while sehe erwarte ich eine>> Wiederholschleife.NichtWichtig schrieb:> ...deren Abbruchbedingung im while klar und deutlich zu sehen ist.
Es ist zweifellos Geschmackssache. Das ist so ein Punkt, den ich hier
mit euch gerne diskutieren, aber nicht mit meinen Arbeitskollegen - da
eigentlich total unwichtig.
Ich erwarte bei einer Schleife, dass sie zumindest manchmal öfter als 1x
durchlaufen wird, was hier nicht der Fall ist. Hier wurde do/while nicht
verwendet, um eine Wiederholung auszulösen.
Stefanus F. schrieb:> Dafür wurde in Java try-finally erfunden. Leider hat C/C++ das nicht.
Mir scheint, dass es das in ObjectPascal/Delphi schon gab, lange bevor
Java als Sprache überhaupt nur das Licht der Welt erblickt hat...
Stefanus F. schrieb:> Hier wurde do/while nicht verwendet, um eine Wiederholung auszulösen.
Stimmt. Aber die alternativen (Endlosschleife oder Switch /dafault) sind
übler.
do while (0) ist quasi der defakto Standard für einmaligen Durchlauf.
Karl schrieb:>> Arduino Fanboy D. schrieb:>> Dumdi D. schrieb:>> array von funktionspointern> ...> In einer Welt in der der geneigte Programmierer zu doof ist, mehrere> returns zu verkraften bewerfen wir ihn mit Funktionspointern um ein> dogmatisches Befolgen von allgemeinen "Weisheiten" zu erreichen? Deine> Kollegen oder Nachfolger werden es danken.
So ein Unfug!
Beim Beispiel des Themenstarters sind Zeiger auf Funktionen
wahrscheinlich der beste Weg einen übersichtlich Code zu bekommen!
Frank M. schrieb:> unsigned int check()> {> unsigned int result;>> if ((result = func1()) == 0 &&> (result = func2()) == 0 &&> (result = func3()) == 0)> {> result = do_something ();> }> return result;> }>> Vorteile:> - Keine verschachtelten if-Statements> - Die Bedingungen stehen übersichtlich untereinander> - Die Reihenfolge der func()-Aurufe ist wie gewünscht> - Abbruch der func()-Aufrufe, sobald ein Fehler zurückkommt> - Keine Klammer-Orgie> - Nur ein return
Nachteil: Es ist auf den ersten Blick schwer nachzuvollziehen was der
Code tut. Und das macht leider alle anderen "Vorteile" zunichte.
Es gibt sicher nicht viele legitime Anwendungsfälle von GOTO, aber hier
wäre es definitv eine Option: Es gibt nur ein return und dennoch ist
sonnenklar was passiert.
Walter Kollascheck schrieb:> sind Zeiger auf Funktionen> wahrscheinlich der beste Weg einen übersichtlich Code zu bekommen!
ich melde dann schon mal wiederspruch an!
ich sehe hier keinen echten sinn/zwang/vorteil von function pointer
verwendung.
mt
Also bei manchen Vorschlägen hier bekommt man echt Augenkrebs. Es geht
darum möglichst wartbaren Code zu schreiben. Da bleibt man dann lieber
gleich bei mehreren returns bevor man solche wilden Konstrukte bastelt.
Peter Petersson schrieb:> Mehrfache Returns sind nicht per se schlecht.
das sehe ich auch so und scheue auch nicht davor zurück wenige/viele -
wenn es sinnig erschein, einzusetzen.
mt
Apollo M. schrieb:> ich melde dann schon mal wiederspruch an!> ich sehe hier keinen echten sinn/zwang/vorteil von function pointer> verwendung.
ich denke dass so ein Array hier sehr wohl Sinn macht.
Mike schrieb:> result = func3();> if (result != 0)> return result;>> ... // viele weitere
Und viele weitere.
Wer will das schon per hand machen.
A. S. schrieb:> Im TO-Beispiel würde ich "Blocksatz" empfehlen:
warum? welche Vorteile bringt das?
besser eine Anweisung pro Zeile als drei (finde ich).
zitter_ned_aso schrieb:> warum? welche Vorteile bringt das?
einen hast Du selbst genannt.
zitter_ned_aso schrieb:> Und viele weitere.>> Wer will das schon per hand machen.
Sobald Du 20-30 davon hast(*) ist Blocksatz besser schreib- und lesbar.
Was gleich ist, ist gleich (copy/paste), nur eine Spalte ist fortlaufend
bzw. individuell zu ändern bzw. geändert.
Du siehst mit einem Blick: Das Muster ist immer gleich, verstehe ich
eine Zeile, verstehe ich alle, keine Sonderbehandlung im 17.ten Fall.
Erst recht, wenn Du mehrere "Spalten" ändern musst, z.B. weil func1 mit
A1, B1, C1 aufgerufen wird.
Ich gehe manchmal sogar hin, und packe das ganze in ein Makro qd (heisst
immer qd) in der Zeile davor, mit undef dahinter, z.B.
1
/* DUMMY_STATEMENT reserviert für das Semikolon am Ende */
Der für mich große Nachteil dabei ist jetzt, dass ich z.B. "PAR1_TYP_1"
im Suchlauf per Editor nicht finde. Darum rolle ich die Schleifen (im
Blocksatz) meist doch aus.
(*) und aus welchenn Gründen auch immer dann trotzdem keine Func-Ptr
willst. Und ja, wenn es geht, nimmt man Arrays auch für Parameter, aber
manchmal geht es halt nicht.
Apollo M. schrieb:> ich sehe hier keinen echten sinn/zwang/vorteil von function pointer> verwendung.
Die Frage ist im Prinzip gleich zu:
Ich habe mehrere ints a1, a2, a3,... und muss damit irgendetwas machen.
Da sagt doch auch jeder nimm ein Array. Der einzige Unterschied ist halt
das nicht jeder 'sattelfest' mit Funktionenpointern ist.
Apollo M. schrieb:> ich sehe hier keinen echten sinn/zwang/vorteil von function pointer> verwendung.
Der Vorteil von Schleifen ist, man muß sich nur einen einzigen Durchlauf
anschauen, verstehen und testen.
Ein weiterer Vorteil ist, man kann sehr bequem Elemente hinzufügen oder
löschen. Man muß dazu nicht mal den Code anfassen, sondern nur das
Array.
Für das Schleifenende nehme ich gerne einen Null-Pointer als letztes
Element. Dann kann die Funktion auch über verschiedene Arrays laufen.
Ich nehme oft schon ab 2 gleichen Aktionen eine Schleife. Damit
vermeidet man optimal copy&paste Fehler.
Peter D. schrieb:> Apollo M. schrieb:>> ich sehe hier keinen echten sinn/zwang/vorteil von function pointer>> verwendung.>> Der Vorteil von Schleifen ist, man muß sich nur einen einzigen Durchlauf> anschauen, verstehen und testen.> Ein weiterer Vorteil ist, man kann sehr bequem Elemente hinzufügen oder> löschen. Man muß dazu nicht mal den Code anfassen, sondern nur das> Array.> Für das Schleifenende nehme ich gerne einen Null-Pointer als letztes> Element. Dann kann die Funktion auch über verschiedene Arrays laufen.> Ich nehme oft schon ab 2 gleichen Aktionen eine Schleife. Damit> vermeidet man optimal copy&paste Fehler.
Man muss sich aber bewusst sein, dass man auf ein Elementtyp beschränkt
ist. Wenn eine der Funktionen plötzlich Argumente braucht, muss man
alles wieder ändern, oder Ausnahmen einführen.
mh schrieb:> Wenn eine der Funktionen plötzlich Argumente braucht, muss man> alles wieder ändern, oder Ausnahmen einführen.
Das sehe ich ebenfalls so.
Aber selbst wenn garantiert werden kann, dass die Funktionen auch
zukünftig immer vom selben Typ sind, gefällt mir hier die Methode ohne
Schleife besser:
Yalu X. schrieb:> gefällt mir hier die Methode ohne> Schleife besser:
Zumindest ist sie schön "rechteckig".
Und damit übersichtlich und leicht um weitere Tests zu erweitern.
Yalu X. schrieb:> Aber selbst wenn garantiert werden kann, dass die Funktionen auch> zukünftig immer vom selben Typ sind, gefällt mir hier die Methode ohne> Schleife besser:
Bei den Trivial-Beispiel mit nur 4 Funktionen ist das so.
Wenn es sehr viel mehr wären (was in dem Kontext allerdings eher selten
der Fall sein wird), hat die Funktionspointer-Schleife schon Vorteile.
Oliver
Oliver S. schrieb:> Yalu X. schrieb:>> Aber selbst wenn garantiert werden kann, dass die Funktionen auch>> zukünftig immer vom selben Typ sind, gefällt mir hier die Methode ohne>> Schleife besser:>> Bei den Trivial-Beispiel mit nur 4 Funktionen ist das so.>> Wenn es sehr viel mehr wären (was in dem Kontext allerdings eher selten> der Fall sein wird), hat die Funktionspointer-Schleife schon Vorteile.
Das ist richtig. Bei mehr als etwa 10 Funktionen würde ich sie auch in
eine Schleife packen. Allerdings hatte ich diesen Fall noch nie. Bei so
vielen Funktionen ist zudem die Chance groß, dass sie untereinander sehr
ähnlich sind, so dass man sie evtl. in eine einzelne Funktion (mit einer
internen Schleife) zusammenfassen kann.
Würde man C endlich mal an die Seite legen und C++ benutzen und auch
einige grundlegende Regeln der Modellierung einhalten (z.B. ein `int`
ist ein Datentyp, der keine ungültigen Werte wie etwa 0 oder -1 besitzt)
würde, dann wäre es allgemein und simpel lösbar:
1
#include<optional>
2
#include<iostream>
3
4
usingvalue_type=std::optional<int>;
5
6
value_typefunc1(){
7
return1;
8
}
9
value_typefunc2(){
10
return2;
11
}
12
13
value_typefunc3(){
14
return{};
15
}
16
17
template<typename...F>
18
value_typecheck(F...ff){
19
value_typeresult;
20
((result=ff())||...);
21
returnresult;
22
}
23
24
intmain(){
25
autor=check(func3,func1,func2);
26
if(r){
27
std::cout<<*r<<'\n';
28
}
29
}
Das Überprüfen zur Compilezeit, ob alle Funktionen tatsächlich vom
selben Typ sind, ist zwar simpel und gehört dahinein, überlasse ich aber
dem geneigten Leser.
Und ja, ich habe gelesen, dass nach C und nicht nach C++ gefragt war.
Nur bei den obigen Vorschlägen ... sorry!
Peter D. schrieb:> Apollo M. schrieb:>> ich sehe hier keinen echten sinn/zwang/vorteil von function pointer>> verwendung.>> Der Vorteil von Schleifen ist, man muß sich nur einen einzigen Durchlauf> anschauen, verstehen und testen.> Ein weiterer Vorteil ist, man kann sehr bequem Elemente hinzufügen oder> löschen. Man muß dazu nicht mal den Code anfassen, sondern nur das> Array.
Bei so einem simplen Progammtext wie eingangs existiert doch nicht
ernsthaft ein Problem mit anschauen oder verstehen.
Was man da ausgerechnet durch extra Arrays, Beschränkungen von
Funktionsnamen auf Nummern oder umständlich noch mit extra Zuweisungen
reißen will, ist mir ein Rätsel.
Ich finde die bisherige Lösung mit mehreren returns immer noch die
beste.
Die ist übersichtlich. Einfach zu erstellen. Jeder andere Programmierer
weiss auch, was dort passiert. Sie hat wenig Overhead.
Warum also sich etwas anderes aus den Fingern saugen, wo die Gefahr
besteht, dass Übersicht verloren geht, oder noch mehr Fehler eingebaut
werden können.
Vincent H. schrieb:> Das Beispiel selbst is eigentlich komplett funktional und schreit> förmlich Monade. Das is in C halt leider eher häßlich und "short> curcuit"-en tuts auch nicht.
Ja, Monaden machen erst dann Spaß, wenn die verwendete Sprache auch den
entsprechenden syntaktischen Zucker dazu liefert.
Dein C-Beispiel
Vincent H. schrieb:> o.and_then(&o, func1)> ->and_then(&o, func2)> ->and_then(&o, func3);
würde bspw. in Haskell zu einem einfachen
1
do
2
func1
3
func2
4
func3
Der Rückgabewert der Funktionen wäre bspw. Either Int (), d.h. im
Fehlerfall geben sie einen Fehlercode als Int zurück, sonst ein Nichts.
Selbst ungezuckert (d.h. ohne do-Notation) ist der Ausdruck in diesem
konkreten Beispiel nicht viel komplizierter:
1
func1 >> func2 >> func3
Liegen die Funktionen bereits in einer Liste vor
1
funcs = [ func1, func, func3 ]
erreicht man dasselbe mit
1
sequence_ funcs
Nur schade, dass es keinen Haskell-Compiler für kleine Mikrocontroller
gibt ;-)
batman schrieb:> Peter D. schrieb:>> Apollo M. schrieb:>>> ich sehe hier keinen echten sinn/zwang/vorteil von function pointer>>> verwendung.>>>> Der Vorteil von Schleifen ist, man muß sich nur einen einzigen Durchlauf>> anschauen, verstehen und testen.>> Ein weiterer Vorteil ist, man kann sehr bequem Elemente hinzufügen oder>> löschen. Man muß dazu nicht mal den Code anfassen, sondern nur das>> Array.>> Bei so einem simplen Progammtext wie eingangs existiert doch nicht> ernsthaft ein Problem mit anschauen oder verstehen.
Der Code hat mehrere Probleme: Datentypen werden falsch verwendet und er
ist nicht generisch. Das mehrfache `return` ist in der Tat gar kein
Problem.
Apollo M. schrieb:> c++?
absolut ja (s.o.).
> für mich ist "Rust" der zukünftige stern am embbeded himmel, ein> würdiger ablöser der c ersthaft ersetzen kann auch im 8bit segment!
Das denke ich auch, es geht schon vieles!
Den Hinweis auf C++ habe ich oben gebracht, weil man damit auch als
C-Programmierer viele Vorteile genießen kann, die wirklich wichtig sind:
domänen-spezifische Datentypen und Generizität.
Bernd K. schrieb:> Yalu X. schrieb:>> Either>> Nicht eher ein Maybe?
Es soll im Abbruchfall ein Errorcode zurückgegeben werden (mit Either
also bspw. ein Left 13). Geht es nur um den Abbruch als solchen, reicht
auch ein Maybe mit Rückgabewert Nothing.
In beiden Fällen könnte auch im Erfolgsfall ein Ergebnis zurückgegeben
werden, so dass bspw. auch folgendes möglich ist:
1
do
2
x <- func1
3
y <- func2
4
z <- func3
5
doSomethingWith x y z
Aber das alles ist ziemlich offtopic, da es in dem Thread ja um C und
nicht um Haskell geht. Ich bin nur deswegen daraufgekommen, weil Vincent
weiter oben das Thema Monade angerissen hat, was zwar ein guter Ansatz
ist, aber nach seiner eigenen Aussage in C eher häßlich aussieht. In
Haskell, wo Monaden ein zentrales Sprachelement darstellen, verschwindet
diese Häßlichkeit komplett. Auch in C# lassen sich Monaden dank LINQ auf
recht ansprechende Weise nutzen. Leider muss man sich in C# m.W. die
benötigten Datentypen wie Maybe oder Either erst selber zusammenbasteln.
Apollo M. schrieb:> Christian B. schrieb:>> Da garantiert Dir übrigens keiner in welcher Reihenfolge>> darum gilt es dann diese variante zu verwenden>> if(c=f(), c && b=f2(), b || ...)>> dann sollte es passen!?
Alleine die Tatsache dass man signifikant darüber nachdenken muss zeigt,
dass es nicht wirklich gut ist. Code muss möglichst sofort verständlich
sein. Jedes Stück Hirnschmalz, welches Du in das Parsen des Codes
investiert, fehlt Dir später beim Verstehen was das Programm überhaupt
macht.
Mike schrieb:> Nun gelten mehrfache returns bekanntlich als schlechter Programmierstil.
Nö.
Nur bei solchen selbsternannten Gurus, denen nichts Besseres eingefallen
ist, um ne zusätzliche Regel zu erfinden.
Das Einzige, was wirklich gilt, ist ob deine Quelle der Syntax von C
entspricht und der Compiler folglich selbige übersetzen kann oder nicht.
Rufus Τ. F. schrieb:> Bestimmte Konstrukte sind mit mehrfachen Returns klar, kurz und> übersichtlich.>> Manche Konstrukte werden durch krampfhaftes Vermeiden mehrfacher Returns> sogar ausgesprochen unübersichtlich.
Dem stimme ich ausdrücklichst zu.
Und man kann das auch erweitern auf einige andere Konstrukte.
W.S.
Yalu X. schrieb:> Auch in C# lassen sich Monaden dank LINQ auf> recht ansprechende Weise nutzen. Leider muss man sich in C# m.W. die> benötigten Datentypen wie Maybe oder Either erst selber zusammenbasteln.
Rust hat das übrigens auch, da heißen sie Option und Result (und man
kann sich selber welche machen, das ist eine clevere Erweiterung des
enum-Datentyps). Aber wie man das obige dann am elegantesten in Rust
schreiben würde kann ich auch nicht sagen, ich bin da noch nicht richtig
eingestiegen. Aber immerhin gibts das auch für ARM Controller.
Christian B. schrieb:> Apollo M. schrieb:>> Christian B. schrieb:>>> Da garantiert Dir übrigens keiner in welcher Reihenfolge>>>> darum gilt es dann diese variante zu verwenden>>>> if(c=f(), c && b=f2(), b || ...)>>>> dann sollte es passen!?>> Alleine die Tatsache dass man signifikant darüber nachdenken muss zeigt,> dass es nicht wirklich gut ist. Code muss möglichst sofort verständlich> sein.
genau! Deswegen sollte man eben möglichst domänen-spezifische Datentypen
kreieren und den Code möglichst generisch halten.
static_assert(isOptional<first_type>::value,"functions must return optional<T>");
35
static_assert((std::is_same<first_type,decltype(ff())>::value&&...),"all functions must return same type");
36
result_typeresult;
37
((result=ff())||...);
38
returnresult;
39
}
40
41
intmain(){
42
autor=first_valid_result_of(func1,func1,func3);
43
if(r){
44
return*r;
45
}
46
return42;
47
}
Die Funktion `first_valid_result_of()` ist generisch mit einigen
Typanforderungen, etwa, dass alle Callables, die als Argumente übergeben
werden, vom selben Typ std::optional<> sind.
Wer die Meta-Funktion isOptional<> und front<> und die Type-Liste und
die static_asserts nicht mag, kann sie gerne entfernen, dann sind wir
wieder beim ursprünglichen Dreizeiler. Allerdings sind sie wichtig,
damit falscher Code ersr gar nicht compiliert.
Wilhelm M. schrieb:> genau! Deswegen sollte man eben möglichst domänen-spezifische Datentypen> kreieren
Jetzt hört doch auf damit. Christian hat sich vertan (Anfänger oder noch
nie gebraucht), Apollo ebenso, und nun werden wilde workarounds für ein
ideomatisches, zuverlässiges Konstrukt diskutiert.
Wenn ein Verhalten explizit definiert ist (hier: Auswertung von links
nach rechts), dann ist es sinnlos, das zu leugnen.
Yalu X. schrieb:> Es soll im Abbruchfall ein Errorcode zurückgegeben werden (mit Either> also bspw. ein Left 13). Geht es nur um den Abbruch als solchen, reicht> auch ein Maybe mit Rückgabewert Nothing.
Nö.
Monaden? Either? Ziemlich am Thema vorbei:
Der Fragesteller möchte nacheinander ein paar Funktionen ausprobieren,
und das Ergebnis der ersten Funktion nutzen die kein Fehler
signalisiert. Signalisieren alle Funktionen einen Fehler, soll wiederum
ein Fehler signalisiert werden.
Weil er in C programmiert, macht er es mit dem Returncode. In Haskell
würde man dafür natürlich ein Maybe verwenden.
Haskell hat Lazy-Evaluation, das macht die Sache Pippi-einfach:
Angenommen, er hat ein Funktionsaufrufe (bzw. Konstanten) die 0 im
Fehlerfall zurückgeben, ist die Lösung:
Das wars. Nix Monade. Einfacher geht's wohl kaum.
Angenommen, die Funktionen geben ein Maybe zurück, kommt ein Monoid zum
Einsatz:
1
let check = getFirst . foldMap First $ [func1 ..., func2 ..., func3 ...]
Wieder keine Monade... Und gibt selbst ein Maybe zurück; macht ja
Sinn...
In der Realität will man die Funktionalität von "check" natürlich nicht
als Konstante, sondern als Funktion. Dann auch mit anderem Namen:
1
let acceptFirstResult x = getFirst . foldMap First . map ($ x)
2
3
let x = ...
4
let check = acceptFirstResult x [func1, func2, func3]
Angenommen eine Funktion gibt ein "Either" zurück, naja, macht ja nix:
1
let x = ...
2
let check = acceptFirstResult x [func1, rightToMaybe . func2, func3]
Hat man tatsächlich Funktionen, die einen Fehler mit einem Wert
signalisieren, baut man sich eben wieder einen kleinen Helper:
1
let toNothing p x = if x == p then Nothing else Just x
2
3
let x = ...
4
let check = acceptFirstResult x [func1, toNothing 0 . func2, func3]
usw., etc.
Merke: Mit Haskell sollte man nur dann um sich werfen, wenn man etwas
mehr als ein Tutorial hinter sich hat...
A. S. schrieb:> Wilhelm M. schrieb:>> genau! Deswegen sollte man eben möglichst domänen-spezifische Datentypen>> kreieren>> Jetzt hört doch auf damit. Christian hat sich vertan (Anfänger oder noch> nie gebraucht), Apollo ebenso, und nun werden wilde workarounds für ein> ideomatisches, zuverlässiges Konstrukt diskutiert.>> Wenn ein Verhalten explizit definiert ist (hier: Auswertung von links> nach rechts), dann ist es sinnlos, das zu leugnen.
Was hat das mit Datentypen zu tun? Was habe ich geleugnet?
zitter_ned_aso schrieb:> wie viele Programmiersprachen könnt ihr??? und wie behält man das alles> im Kopf / dabei nicht durcheinander kommt?
DOS/Windows Batch, GwBasic, C64 Basic, Visual Basic, Visual Basic for
Applications, Bash, SED, AWK, Pascal, C, C++, Assembler, PL/SQL, Python,
Java, Javascript, Perl, TCL/TK, PHP, ... (Ich habe sicher noch welche
vergessen)
Manchmal werfe ich etwas durcheinander. Die spezifischen
Entwicklungsumgebungen helfen dabei, wenigstens die Syntax einzuhalten.
Und Google - ohne Internet und Google wäre ich aufgeschmissen. Die
Zeiten von Turbo Pascal unter DOS, wo man das ganze Framework fast
auswendig lernen konnte, sind definitiv vorbei.
Man programmiert meistens mit dem, was einem vorgeschrieben wird.
Flexibilität hat noch keinem geschadet, so ist man weniger arbeitslos.
Echtter Programmierer schrieb:> Yalu X. schrieb:>> Es soll im Abbruchfall ein Errorcode zurückgegeben werden (mit Either>> also bspw. ein Left 13). Geht es nur um den Abbruch als solchen, reicht>> auch ein Maybe mit Rückgabewert Nothing.> Der Fragesteller möchte nacheinander ein paar Funktionen ausprobieren,> und das Ergebnis der ersten Funktion nutzen die kein Fehler> signalisiert. Signalisieren alle Funktionen einen Fehler, soll wiederum> ein Fehler signalisiert werden.
Ups, da habe ich den Eröffnungsbeitrag nicht aufmerksam genug gelesen.
Ich bin irrtümlicherweise davon ausgegangen, dass 0 Erfolg signalisiert
und von 0 verschiedene Werte einen Fehlercode darstellen (ich hatte noch
einen anderen Thread im Hinterkopf, wo es tatsächlich so war).
Das ändert aber nichts daran, dass meine Code-Beispiele das gewünschte
Verhalten (vorzeitiger Abbruch bei validem Funktionswert) und Ergebnis
(erster valider Funktionswert) liefern. Lediglich meine Interpretation
der Ergebnisse in den begleitenden Texten sind verkehrt herum. Ersetze
in diesen Texten "Erfolg" (und ähnliche Begriffe) durch "Fehler" und
umgekehrt, dann sollte das Meiste stimmen.
> Monaden? Either? Ziemlich am Thema vorbei:
Streng genommen sind alle Beiträge zum Thema Haskell, C++, Python, Rust
usw. am Thema vorbei, da der TE nach C fragte. Nachdem aber so ziemlich
alle denkbaren Alternativlösungen in C längst abgehakt sind, ist es auch
nicht schlimm, wenn der Themenhorizont etwas erweitert wird.
Was das Thema Monaden betrifft, bezog ich mich dabei darauf:
Vincent H. schrieb:> Das Beispiel selbst is eigentlich komplett funktional und schreit> förmlich Monade.
Ich habe übrigens nirgends behauptet, dass es nicht auch ohne Monade
geht.
> Weil er in C programmiert, macht er es mit dem Returncode. In Haskell> würde man dafür natürlich ein Maybe verwenden.
Ach, und was ist Maybe?
1
>> :i Maybe
2
...
3
instance Monad Maybe
4
...
> Haskell hat Lazy-Evaluation, das macht die Sache Pippi-einfach:>> Angenommen, er hat ein Funktionsaufrufe (bzw. Konstanten) die 0 im> Fehlerfall zurückgeben, ist die Lösung:> let check = fromMaybe 0 . find (/=0) $ [func1 ..., func2 ..., func3 ...]
Was ist daran jetzt so viel einfacher als bei
1
check = do
2
func1
3
func2
4
func3
Ja, mir ist klar, dass die beiden Beispiele nicht ganz identisch sind,
da bei dir die Rückgabewerte von der Typklasse Num und Eq, bei mir
hingegen vom Typ Either a () sind. Dabei kann a ein beliebiger Typ sein,
und für den Erfolgsfall steht der komplette Wertebereich dieses Typs zur
Verfügung. Bei dir (wie auch beim TE) hat die 0 einen Sonderstatus, den
man eigentlich vermeiden möchte, da er die Generizität einschränkt.
Warum sollte man also der Lösung, die kürzer, übersichtlicher und dazu
noch generischer ist, nicht den Vorzug geben?
Außerdem sind Sprachfeatures (in diesem Fall Monaden) dazu da, um
genutzt zu werden, und nicht um zu zeigen "ich kann's auch ohne".
> Merke: Mit Haskell sollte man nur dann um sich werfen, wenn man etwas> mehr als ein Tutorial hinter sich hat...
Ja, ja, arbeite erst einmal die zweite Hälfte des Tutorials durch, dann
wirst du viele Dinge besser verstehen :)
Yalu X. schrieb:> Ach, und was ist Maybe?>>>> :i Maybe> ...> instance Monad Maybe> ...
Und? Wo in meinen Beispielen wird der monadische Berechnung verwendet?
Nirgends!
> Was ist daran jetzt so viel einfacher als bei>> check = do> func1> func2> func3
Naja, es ist halt falsch...
Sowohl Maybe-Monade wie auch Either-Monade machen etwas ganz anderes.
Sie brechen ab, sobald eine Berechnung "Nothing" bzw. "Left" ergibt.
Dein obiges Beispiel ergibt in der Maybe-Monade Nothing falls func1 oder
func2 Nothing sind, ansonsten das Resultat von func3. In der
Either-Monade genau das gleiche, nur eben mit Left für func1 und func2.
Probier's doch einfach aus, wenn Du ghci eh schon auf hast:
[code]
> Just 1 >> Just 2 >> Just 3
Just 3
> Just 1 >> Nothing >> Just 3
Nothing
[/code}
Die Aufgabe ist aber eine andere. Nämlich das erste Just aus einer Reihe
von Maybes oder eben das erste Right aus einer Reihe von Eithers.
> Ja, ja, arbeite erst einmal die zweite Hälfte des Tutorials durch, dann> wirst du viele Dinge besser verstehen :)
Bis jetzt bist Du mit bezüglich Haskell ziemlich unterlegen, wie Du hier
selbst demonstrierst.
Echter Programmierer schrieb:> Die Aufgabe ist aber eine andere. Nämlich das erste Just aus einer Reihe> von Maybes oder eben das erste Right aus einer Reihe von Eithers.
Tip halt mal folgendes in ghci und lerne:
1
> getFirst . foldMap First $ [Just 1, Nothing, Just 3]
2
Just 1
3
4
> getFirst . foldMap First $ [Nothing, Nothing, Just 3]
5
Just 3
6
7
> getFirst . foldMap First $ [Nothing, Nothing, Nothing]
Echter Programmierer schrieb:> Tip halt mal folgendes in ghci und lerne:
Das geht jetzt hier total am Thema vorbei. Eröffne bitte dazu einen
neuen Thread, um Vor-/Nachteile von Haskell & Co anhand von Beispielen
anzuführen ... und Dein Können zu demonstrieren.
Echter Programmierer schrieb:> Sowohl Maybe-Monade wie auch Either-Monade machen etwas ganz anderes.> Sie brechen ab, sobald eine Berechnung "Nothing" bzw. "Left" ergibt.
Von Maybe habe ich nichts geschrieben, sondern von Either, das wie folgt
parametriert ist:
Yalu X. schrieb:> Der Rückgabewert der Funktionen wäre bspw. Either Int ()
Oder allgemeiner:
Yalu X. schrieb:> vom Typ Either a ()
D.h. das Ergebnis wird in Left zurückgegeben, was, wie du richtig
schreibst, zum Abbruch führt.
Wenn du das Thema weiter ausbreiten möchtest, starte – wie von Frank
vorgeschlagen – einen neuen Thread, damit dieser hier nicht zu sehr
überladen wird. Ich bin dann gerne mit dabei.
Yalu X. schrieb:> das Ergebnis wird in Left zurückgegeben, was, wie du richtig> schreibst, zum Abbruch führt.
Was ziemlich dämlich ist, da per Konvention Right das Ergebnis ist und
Left ein Fehler.
Aber gegen zwei Moderatoren kann ich nichts ausrichten und beende
hiermit...
Echter Programmierer schrieb:> Bis jetzt bist Du mit bezüglich Haskell ziemlich unterlegenEchter Programmierer schrieb:> Aber gegen zwei Moderatoren
Das ist hier kein Wettbewerb oder so etwas. Mach einfach einen neuen
Haskell-Thread auf und gut ist.
Wilhelm M. schrieb:> genau! Deswegen sollte man eben möglichst domänen-spezifische Datentypen> kreieren und den Code möglichst generisch halten.
Darauf hab ich gewartet :) Aus Spaß im Anhang eine aufgetunte Version
davon.
Nutzungsbeispiel:
Features:
- Kann beliebige Parameter an die Funktionen übergeben (zweites
Klammerpaar)
- Ist constexpr
- Funktioniert mit beliebigem Rückgabetyp, sofern dieser nach "bool"
konvertierbar und move-constructible ist
- Der Rückgabetyp wird automatisch ermittelt und zurückgegeben
- Es wird der erste Rückgabewert zurückgegeben, der nach bool
konvertiert "true" ist
- Liefert kein Wert "true", wird ein per Standardkonstruktor erstellter
Rückgabewert zurückgegeben
- Es wird nicht 1 Instanz des Rückgabetyps erstellt und wiederholt
zugewiesen, sondern immer nur 1 Instanz erstellt, geprüft, und
zurückgegeben oder zerstört. Funktioniert somit auch mit
nicht-zuweisbaren oder nicht-kopierbaren Typen.
- Funktioniert somit z.B. mit Integer-Rückgabetypen (mit 0 = "Fehler")
und mit std::optional oder std::unique_ptr
- Hat keine Abhängigkeiten außer C++17-Standardbibliothek
Niklas G. schrieb:> Wilhelm M. schrieb:>> genau! Deswegen sollte man eben möglichst domänen-spezifische Datentypen>> kreieren und den Code möglichst generisch halten.>> Darauf hab ich gewartet :) Aus Spaß im Anhang eine aufgetunte Version> davon.
Dankeschön! Endlich hat es jemand verstanden ;-)
In meinen Augen sind das tolle Toy-Examples. In c++ hat man für soetwas
Exceptions parat. Da gibt es keinen Grund, das gesamte Programm in ein
einzelnes Statement zu stopfen.
Wo sollten die Kommentare stehen? Triviale Frage - "Kommentare schreibe
ich sowieso nie".
Heiko L. schrieb:> In c++ hat man für soetwas Exceptions parat.
In Java sind Exceptions "teuer" ,deswegen meidet man dort deren
Benutzung für normale Fälle, die häufig auftreten. Sind Exceptions in
C++ nicht teuer?
Heiko L. schrieb:> In meinen Augen sind das tolle Toy-Examples.
Lass uns doch den Spaß.
Heiko L. schrieb:> In c++ hat man für soetwas> Exceptions parat.
An sich schon, aber auf Embedded-Targets haben die leider oft zu viel
Overhead.
Niklas G. schrieb:> Heiko L. schrieb:>> In meinen Augen sind das tolle Toy-Examples.>> Lass uns doch den Spaß.>> Heiko L. schrieb:>> In c++ hat man für soetwas>> Exceptions parat.>> An sich schon, aber auf Embedded-Targets haben die leider oft zu viel> Overhead.
Dies ist m.E. ein gutes Beispiel, um keine Exceptions zu verwenden. Denn
das Zurückgeben der "leeren Menge" als Resultat ist (oder könnte
zumindest) der Nornmalfall sein. (Zudem sind Exception der Tod jedes
Optimizers).
Stefanus F. schrieb:> Sind Exceptions in> C++ nicht teuer?
Ja, einige Größenordnungen langsamer als eine return-chain. Deswegen
empfiehlt sich, Exception auch wirklich nur für "Ausnahmen" zu benutzen.
In normalen Programmen daher besonders geeignet für Fehlerconditions,
die eigentlich nicht auftreten sollten.
Es gäbe auch dort Argumente für eine return-chain, wie zB, dass man
sonst ja nicht so schön die Paramter, die zum Fehler führten, loggen
kann. :)
Wilhelm M. schrieb:> Dies ist m.E. ein gutes Beispiel, um keine Exceptions zu verwenden. Denn> das Zurückgeben der "leeren Menge" als Resultat ist (oder könnte> zumindest) der Nornmalfall sein. (Zudem sind Exception der Tod jedes> Optimizers).
Also für mich sieht das initiale Beispiel des Codes so aus, als ob sich
der OP eigentlich nicht um die Fehler kümmern will, die da zurück kommen
könnten. Das ist erschreckend häufig der Fall - oft kann man einfach
nichts vernünftiges tun. Beispiel: Man schreibt eine Datei und auf
einmal ist die Festplatte voll. Wie oft kommt dieser Fall vor? Soll jede
Funktion, die I/O betreibt, versuchen, diesen Fall zu handeln?
Hi,
eine Lösung mit switch ... case kommt nicht infrage?
Man könnte alle Fälle mit einer for Schleife durchgehen und in einem
Ergebniss Vektor ablegen.
So würde man auch erkennen ob mehrer Funktionen ein Ergebniss
zurückgeben und diese könnte man dann gegenüber stellen.
Viele Grüße
Heiko L. schrieb:> Es gäbe auch dort Argumente für eine return-chain, wie zB, dass man> sonst ja nicht so schön die Paramter, die zum Fehler führten, loggen> kann. :)
Im Bereich der Backend Anwendungen setzte ich da auf Ringpuffer. Jeder
Threads bekommt einen Ringpuffer, der bis zu 100 Meldungen (ab Debug
Level aufwärts) aufnimmt. Wenn ein Fehler auftritt, wird dieser zusammen
mit den gepufferten Meldungen in eine Datei geschrieben.
Das kostet RAM und (typischerweise 1-5%) Performance, lohnt sich aber,
denn so kann man Probleme viel besser analysieren. Vorausgesetzt, man
geizt nicht mit Debug Meldungen.
Heiko L. schrieb:> Wilhelm M. schrieb:>> Dies ist m.E. ein gutes Beispiel, um keine Exceptions zu verwenden. Denn>> das Zurückgeben der "leeren Menge" als Resultat ist (oder könnte>> zumindest) der Nornmalfall sein. (Zudem sind Exception der Tod jedes>> Optimizers).>> Also für mich sieht das initiale Beispiel des Codes so aus, als ob sich> der OP eigentlich nicht um die Fehler kümmern will, die da zurück kommen> könnten.
Oh je. Das hoffe ich mal nicht.
Aber es müssen nicht unbedingt harte Fehler sein. Es kann auch z.B. eine
Suchoperation sein, die nichts findet und deswegen eine leere Menge
liefert.
Stefanus F. schrieb:> Heiko L. schrieb:>> Es gäbe auch dort Argumente für eine return-chain, wie zB, dass man>> sonst ja nicht so schön die Paramter, die zum Fehler führten, loggen>> kann. :)>> Im Bereich der Backend Anwendungen setzte ich da auf Ringpuffer. Jeder> Threads bekommt einen Ringpuffer, der bis zu 100 Meldungen (ab Debug> Level aufwärts) aufnimmt. Wenn ein Fehler auftritt, wird dieser zusammen> mit den gepufferten Meldungen in eine Datei geschrieben.>> Das kostet RAM und (typischerweise 1-5%) Performance, lohnt sich aber,> denn so kann man Probleme viel besser analysieren. Vorausgesetzt, man> geizt nicht mit Debug Meldungen.
Das glaube ich auf's Wort. Sas mit dem Logging ist nach meiner Erfahrung
leider tragisch: Es ist immer zu wenig - auch wenn es zu viel ist.
Wilhelm M. schrieb:> Aber es müssen nicht unbedingt harte Fehler sein. Es kann auch z.B. eine> Suchoperation sein, die nichts findet und deswegen eine leere Menge> liefert.
Ja, da würde ich persönlich aber keine Exception werfen. Es sei denn ich
wäre der Meinung, dass an der Stelle ein Ergebnis gefunden werden
müsste. Das wäre dann ein logic_error.
Heiko L. schrieb:> Wilhelm M. schrieb:>> Aber es müssen nicht unbedingt harte Fehler sein. Es kann auch z.B. eine>> Suchoperation sein, die nichts findet und deswegen eine leere Menge>> liefert.>> Ja, da würde ich persönlich aber keine Exception werfen.
Ja, das sage ich doch!!!
Schau Dir meine Lösung an, da ist nichts mit Exceptions. std::optional
ist eine generische Lösung, mit der man jeden Datentyp um die leere
Menge / das Nichts erweitern kann (nullable types).
Wilhelm M. schrieb:> Schau Dir meine Lösung an, da ist nichts mit Exceptions.
Man könnte sie aber leicht umbauen, dass sie bei Exceptions jeweils die
nächste Funktion nimmt. Das würde eine lange
try-catch-Kette/Verschachtelung ersparen (sofern man es als sinnvoll
erachtet, das überhaupt mit Exceptions zu signalisieren; eigentlich
sollte man nur sehr wenige catch-Blöcke haben). Das wäre dann aber nicht
mehr constexpr ;-)
Niklas G. schrieb:> Wilhelm M. schrieb:>> Schau Dir meine Lösung an, da ist nichts mit Exceptions.>> Man könnte sie aber leicht umbauen, dass sie bei Exceptions jeweils die> nächste Funktion nimmt.
Warum sollte man das tun?
Wilhelm M. schrieb:> Ja, das sage ich doch!!!> Schau Dir meine Lösung an, da ist nichts mit Exceptions. std::optional> ist eine generische Lösung, mit der man jeden Datentyp um die leere> Menge / das Nichts erweitern kann (nullable types).
Ja - und schau dir den Code des OP an. Da wird 3 mal nacheinander im
Falle eines Fehlers nichts getan, als dieses Ergebnis weiter hoch zu
reichen.
Und das setzt sich typischerweise noch 10-20 Stackframes weiter fort.
Genau dafür gibt es Exceptions. Da muss man dann auch nicht ganz oben
rätseln, was denn der eigentliche Fehler wohl war: Diese Information
verliert man mit Optionals. Und auch Result-Tupel wären nur sehr
unwesentlich besser: Was soll denn der allgemeine Fehlertyp sein, den
man ohne Verrenkungen oder Informationsverlust IMMER weitergeben kann?
Errno?
EDIT: Das soll nicht heißen, dass der Compiler intern ein throw/catch
nicht mit Result-Tupeln handeln sollte. :)
Heiko L. schrieb:> Wilhelm M. schrieb:>> Ja, das sage ich doch!!!>> Schau Dir meine Lösung an, da ist nichts mit Exceptions. std::optional>> ist eine generische Lösung, mit der man jeden Datentyp um die leere>> Menge / das Nichts erweitern kann (nullable types).>> Ja - und schau dir den Code des OP an. Da wird 3 mal nacheinander im> Falle eines Fehlers nichts getan, als dieses Ergebnis weiter hoch zu> reichen.
Und genau dafür habe ich m.E. bessere Lösung.
> Und das setzt sich typischerweise noch 10-20 Stackframes weiter fort.
Woher weißt Du das?
> Genau dafür gibt es Exceptions. Da muss man dann auch nicht ganz oben> rätseln, was denn der eigentliche Fehler wohl war: Diese Information> verliert man mit Optionals.
Das ist auch nicht der Zweck von optional (s.o.).
>Und auch Result-Tupel wären nur sehr> unwesentlich besser:
Variant wäre besser.
Wilhelm M. schrieb:> Heiko L. schrieb:>> Wilhelm M. schrieb:>>> Ja, das sage ich doch!!!>>> Schau Dir meine Lösung an, da ist nichts mit Exceptions. std::optional>>> ist eine generische Lösung, mit der man jeden Datentyp um die leere>>> Menge / das Nichts erweitern kann (nullable types).>>>> Ja - und schau dir den Code des OP an. Da wird 3 mal nacheinander im>> Falle eines Fehlers nichts getan, als dieses Ergebnis weiter hoch zu>> reichen.>> Und genau dafür habe ich m.E. bessere Lösung.
Ja, finde ich nicht.
>> Und das setzt sich typischerweise noch 10-20 Stackframes weiter fort.>> Woher weißt Du das?
Konkret? Gar nicht. Woher weißt du, dass es nicht so ist?
>> Genau dafür gibt es Exceptions. Da muss man dann auch nicht ganz oben>> rätseln, was denn der eigentliche Fehler wohl war: Diese Information>> verliert man mit Optionals.>> Das ist auch nicht der Zweck von optional (s.o.).
Richtig. Deswegen sind die auch ungeeignet für Fehlerbehandlungen.
>>Und auch Result-Tupel wären nur sehr>> unwesentlich besser:>> Variant wäre besser.
Nicht echt. Was ist der allgemeine Fehlertyp?
Heiko L. schrieb:> Wilhelm M. schrieb:>> Heiko L. schrieb:>>> Wilhelm M. schrieb:>>>> Ja, das sage ich doch!!!>>>> Schau Dir meine Lösung an, da ist nichts mit Exceptions. std::optional>>>> ist eine generische Lösung, mit der man jeden Datentyp um die leere>>>> Menge / das Nichts erweitern kann (nullable types).>>>>>> Ja - und schau dir den Code des OP an. Da wird 3 mal nacheinander im>>> Falle eines Fehlers nichts getan, als dieses Ergebnis weiter hoch zu>>> reichen.>>>> Und genau dafür habe ich m.E. bessere Lösung.>> Ja, finde ich nicht.
Es geht hier m.E. gar nicht um Fehlerbehandlung. Mein Ansatz war (s.o.),
dass der DT `int` keine ungültigen Wert besitzt. Es wird hier
willkürlich der Wert 0 missbraucht, um die leere Menge darzustellen.
Dafür braucht man nullable-types. Die primitiven DT ausser raw pointer
sidn nicht nullable. Genau diese Eigenschaft fügt optional hinzu.
Der zweite Ansatz war es, eine generische Lösung zu liefern.
Mit Deiner Forderung nach Exceptions machst Du ein ganz andere Fass auf.
Fehlerbehandlung und nullable-Types sind unterschiedliche Sachen.
Fehlerbehandlung kann man mit Exceptions oder ohne Exceptions machen
(wobei exception-safety in C++ ja ein besonderes Thema ist), dass will
ich gar nicht vorschreiben (wobei die Tendenz ganz klar in Richtung
no-exceptions geht, weil man erkannt hat, dass exceptions auch nicht das
Gelbe vom Ei sind, und dies ein Kapitel, was in C++ problematisch ist,
zumindest wenn man sich vielen existierenden Code ansieht).
Wilhelm M. schrieb:> Es geht hier m.E. gar nicht um Fehlerbehandlung. Mein Ansatz war (s.o.),> dass der DT `int` keine ungültigen Wert besitzt. Es wird hier> willkürlich der Wert 0 missbraucht, um die leere Menge darzustellen.
Richtig. Dafür wären Optionals durchaus brauchbar.
Wilhelm M. schrieb:> Der zweite Ansatz war es, eine generische Lösung zu liefern.
Ja - und das mag hier konkret irgendwie genügen. Es ist aber kein
allgemein gangbarer Weg. Wenn man den return-Wert aus einer Struct
auslesen müsste oder auch nur einen int casten wäre man wieder genau am
Anfang.
Ebenso, wenn man irgenwo noch einen anderen Parameter braucht.
Also ist diese Lösung - entschuldigung! - in meinen Augen ein
Toy-Example, dass für den konkreten Einzelfall viel zu aufwändig und im
Allgemeinen unbrauchbar ist.
Mit lambdas käme man einen Schritt weiter:
1
auto z = first_of(
2
[]{ return x(1); },
3
[]{ return y(2).member; },
4
);
Aber auch da würde ich mich nicht an Optionals binden. Generischen Code
schreibt man, indem man sich konzeptionell bindet :)
Niklas G. schrieb:> Wilhelm M. schrieb:>> genau! Deswegen sollte man eben möglichst domänen-spezifische Datentypen>> kreieren und den Code möglichst generisch halten.>> Darauf hab ich gewartet :) Aus Spaß im Anhang eine aufgetunte Version> davon.
Hab das mal aus Spaß in D nachgebaut.