Heute möchte ich eine weitere Möglichkeit zur Vermeidung von bedingten Verzweigungen mittels if vorstellen. Außerdem möchte ich hier an alle Softwareentwickler den Appell richten, doch bitte kleine Funktionen zu schreiben.
Meine diesbezügliche Leidensgeschichte* beginnt mit einem Absturz aufgrund eines Timeouts. Eine Routine braucht länger als vorgesehen und wird nach Ablauf der vorgeschriebenen Zeit vom überwachenden Thread abgeschossen.
Was ist die Ursache? Eine aufwändige Berechnung? Eine Endlosschleife? Die einzige Debugmöglichkeit ist das Einfügen von printf-Anweisungen, um die Stelle zu finden, an der das Programm zum Stehen kommt. Sehr hilfreich wären hier jetzt kleine Integrationsfunktionen, die keine Logik enthalten und nur andere Funktionen aufrufen. Zwischen die Funktionsaufrufe jeweils ein printf eingefügt, ausführen und sehen, welches printf nicht mehr aufgerufen wird. Daraufhin in die davor ausgeführte Funktion abtauchen, bis man an der Wurzel des Übels angekommen ist. Aber ach!
Mehrere hundert Zeilen lange Funktionen erwarten mich, mit ineinander verschachtelten Verzweigungen und Schleifen. Also wird das ganze zu einen Ratespiel: Ein printf am Funktionsanfang, eines irgendwo etwa in der Mitte und eines am Funktionsende. Wird das mittlere printf noch ausgeführt, nicht aber das zum Schluss, steckt der Fehler in der zweiten Hälfte. Fehlt schon die Ausgabe des mittleren printfs muss die Quelle in der oberen Hälfte gesucht werden. Und so schränkt man weiter ein, bis man auf einen Funktionsaufruf stößt, in diese Funktion abtaucht und dort dann so weitermacht.
Und irgendwann kommt man dann am Ende der Suche an: Ein Systemaufruf, der eine Ressource anfordert und nicht mehr zurück kommt. Und warum kommt er nicht zurück? Weil sehr wahrscheinlich die Ressource an anderer Stelle schon einmal angefordert, aber danach nicht wieder freigegeben wurde. Wir haben also nur die Stelle gefunden, an der sich der Fehler bemerkbar macht. Verursacht wird er aber woanders - durch Unterlassung. Finde den Code, der da sein sollte, aber fehlt!
Also suche ich als nächstes überall im Code danach, wo die entsprechende Ressource angefordert wird. Und das sind einige Stellen. Und sie befinden sich in hunderte Zeilen langen Funktionen mit verschachtelten Bedingungen und Schleifen und mehreren Austrittspunkten. Und ich verstehe schon - auch wenn ich die Stelle noch nicht gefunden habe - warum dieser Fehler gemacht wurde. Am Anfang einer solchen Funktion wird die Ressource reserviert, sie wird im Laufe der langen Funktion genutzt und an Unterfunktionen weitergereicht. Und hin und wieder - so etwa 5 Mal - wird die Funktion vor ihrem Ende durch ein Return verlassen. Und vor jedem Return muss natürlich die Ressource wieder freigegeben werden. Das ist durch die Größe dieser Funktionen und deren Verschachtelungstiefe aber so unübersichtlich, dass ich ganz sicher auf ein Return stoßen werde, bei dem eine solche Freigabe schlicht vergessen wurde.
Und so war es auch. Vier Returns habe ich gefunden (in verschiedenen Funktionen), bei denen die Ressourcenfreigabe fehlte.
Nun mache ich mir aber natürlich Gedanken, wie denn der Code hätte aussehen sollen, damit dieser Fehler hätte vermieden werden können. Wenn die jeweilige Sprachen entsprechende Unterstützung für solche Open/Close-Bereiche anbietet, sollte man diese auf jeden Fall nutzen. In Python wäre dies z.B. das Schlüsselwort with oder in anderen Sprachen die finally-Sektion eines try-Blocks, die garantieren, dass nötige Aufräumarbeiten auf jeden Fall erfolgen.
Aber in C gibt es keine Exceptions und in C++ sind diese teuer und verbieten sich für Echtzeitanwendungen. Hier (und prinzipiell auch in allen anderen Sprachen) sollten Funktionen kurz und übersichtlich bleiben und eine Ressourcenfreigabe sich im Ideal nur zwei Zeilen unter deren Anforderung befinden.
void write_to_file(file_path, data)
{
file_handler = open(file_path);
do_write_action(file_handler, data);
close(file_handler);
}
Ich überflog also die ellenlangen Funktionen und überlegte mir, wie ich sie verkleinern würde - unabhängig von der Ressourcenproblematik. Dabei stieß ich aber auf das Problem, dass, selbst wenn ich alle Verschachtlungen auflöse und in eine lineare Struktur bringe, ich es mit einer Funktion zu tun habe, die den Status der vorherigen Operationen abfragt und bei Misserfolg die weitere Abarbeitung abbricht und die Funktion vorzeitig verlässt. Mit Exceptions wäre das kein Problem, aber ohne wird dies über Rückgabewerte geregelt und sieht - idealtypisch - so aus:
oid do_write_action(file_handler, data)
{
if (subfunction_1(file_handler, data) == false)
{
return;
}
if (subfunction_2(file_handler, data) == false)
{
return;
}
if (subfunction_3(file_handler, data) == false)
{
return;
}
if (subfunction_4(file_handler, data) == false)
{
return;
}
subfunction_5(file_handler, data);
}
Klar, das kann man so lassen. Das ist einigermaßen übersichtlich. Aber aus einer rein theoretisch-dogmatischen Sicht stört mich hier die Vermischung der verschiedenen Abstraktionsniveaus. Integrationen (Funktionsaufrufe) und Operationen (IFs) immer im Wechsel.
Mit Exceptions würde es - meiner Meinung nach - etwas sauberer aussehen:
void do_write_action(file_handler, data) { try { subfunction_1(file_handler, data) subfunction_2(file_handler, data) subfunction_3(file_handler, data) subfunction_4(file_handler, data) subfunction_5(file_handler, data) } catch (UnimportantException) { // Do nothing } }
Wie könnte ich ein ähnliches Aussehen in C erreichen? Gestern beim Einschlafen fiel mir nun die Lösung ein. Sicher, es ist immer noch eine Vermischung von Integration und Operationen, aber kompakter. Evtl. deswegen aber weniger klarer und deswegen im Sinne der Übersichtlichkeit vielleicht doch nicht geeignet. Dennoch möchte ich sie hier vorstellen, wenn auch nur der Dokumentation wegen:
void do_write_action(file_handler, data)
{
subfunction_1(file_handler, data) &&
subfunction_2(file_handler, data) &&
subfunction_3(file_handler, data) &&
subfunction_4(file_handler, data) &&
subfunction_5(file_handler, data);
}
* Dieses "Leiden" ist ein zweischneidiges Schwert. Während ich bei der Fehlersuche bin, fluche und schimpfe ich. Aber wenn dann die Fehlerursache gefunden ist, stellt sich das Hochgefühl des Erfolgs ein, die Welt von einem bösartigen Monster(chen) befreit zu haben. Leiden steckt halt auch in Leidenschaft.
Tatsächlich habe ich dieses && Pattern auch schon mal in echtem kommerziellen C++ Code gesehen und nicht so richtig verstanden, wozu es gemacht wird. Jetzt weiß ich es, Dank dir!
AntwortenLöschen