Gültige Verwendung von goto für das Fehlermanagement in C?


90

Diese Frage ist eigentlich das Ergebnis einer interessanten Diskussion unter programmieren.reddit.com vor einiger Zeit. Es läuft im Grunde auf den folgenden Code hinaus:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Die Verwendung von gotohier scheint der beste Weg zu sein, was zu dem saubersten und effizientesten Code aller Möglichkeiten führt, oder zumindest scheint es mir so. Zitieren von Steve McConnell in Code Complete :

Das goto ist in einer Routine nützlich, die Ressourcen zuweist, Operationen an diesen Ressourcen ausführt und dann die Ressourcen freigibt. Mit einem goto können Sie in einem Abschnitt des Codes bereinigen. Das Goto verringert die Wahrscheinlichkeit, dass Sie vergessen, die Ressourcen an jedem Ort freizugeben, an dem Sie einen Fehler feststellen.

Eine weitere Unterstützung für diesen Ansatz stammt aus dem Linux-Gerätetreiberbuch in diesem Abschnitt .

Was denken Sie? Ist dieser Fall eine gültige Verwendung für gotoin C? Würden Sie andere Methoden bevorzugen, die komplizierteren und / oder weniger effizienten Code erzeugen, aber vermeiden goto?



@Eli: Warum entfernst du nicht die Tags und platzierst die Funktion (cleanup_3 ();) in der Klammer von if?
Fuddin

@ Akito: Was meinst du? Könnten Sie Ihren Vorschlag als Antwort mit einem vollständigen Codebeispiel veröffentlichen?
Eli Bendersky

@ EliBendersky: Bitte sehen Sie meine Antwort.
Fuddin

Eines der Dinge, die ich von Visual Basic am meisten hasste (VBS und VB6 eingeschlossen), war das on error goto errorFehlerbehandlungssystem :)
Manu343726

Antworten:


60

FWIF, ich finde die Fehlerbehandlungssprache, die Sie im Beispiel der Frage angegeben haben, lesbarer und verständlicher als jede der bisher in den Antworten angegebenen Alternativen. Obwohl dies gotoim Allgemeinen eine schlechte Idee ist, kann es für die Fehlerbehandlung nützlich sein, wenn es auf einfache und einheitliche Weise durchgeführt wird. In dieser Situation wird es, obwohl es sich um eine handelt goto, klar definiert und mehr oder weniger strukturiert verwendet.


1
Kann er diese Tags nicht einfach entfernen und die Funktionen direkt einfügen, wenn? Wird es nicht besser lesbar sein?
Fuddin

8
@StartupCrazy, ich weiß, dass dies Jahre alt ist, aber aus Gründen der Gültigkeit der Beiträge auf dieser Website werde ich darauf hinweisen, dass nein, er kann nicht. Wenn er bei goto error3 in seinem Code einen Fehler erhält, führt er die Bereinigung 1 2 und 3 aus. In Ihrer Lösung wird nur die Bereinigung 3 ausgeführt. Er könnte Dinge verschachteln, aber das wäre nur das Pfeil-Antimuster, etwas, das Programmierer vermeiden sollten .
Gbtimmon

17

In der Regel ist es eine gute Idee, goto zu vermeiden, aber die Missbräuche, die vorherrschten, als Dijkstra zum ersten Mal "GOTO als schädlich" schrieb, kommen den meisten Menschen heutzutage nicht einmal in den Sinn.

Was Sie skizzieren, ist eine verallgemeinerbare Lösung für das Problem der Fehlerbehandlung - es ist in Ordnung für mich, solange es sorgfältig verwendet wird.

Ihr spezielles Beispiel kann wie folgt vereinfacht werden (Schritt 1):

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Fortsetzung des Prozesses:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

Dies entspricht meines Erachtens dem ursprünglichen Code. Dies sieht besonders sauber aus, da der ursprüngliche Code selbst sehr sauber und gut organisiert war. Oft sind die Codefragmente nicht so ordentlich (obwohl ich ein Argument akzeptieren würde, dass sie sein sollten); Beispielsweise muss häufig mehr Status an die Initialisierungsroutinen (Setup-Routinen) übergeben werden als gezeigt, und daher muss auch mehr Status an die Bereinigungsroutinen übergeben werden.


23
Ja, die verschachtelte Lösung ist eine der praktikablen Alternativen. Leider wird es weniger lebensfähig, wenn sich die Verschachtelungsebene vertieft.
Eli Bendersky

4
@eliben: Einverstanden - aber die tiefere Verschachtelung könnte (wahrscheinlich) ein Hinweis darauf sein, dass Sie mehr Funktionen einführen oder die Vorbereitungsschritte mehr ausführen lassen oder Ihren Code auf andere Weise umgestalten müssen. Ich könnte argumentieren, dass jede der Vorbereitungsfunktionen ihre Einrichtung vornehmen, die nächste in der Kette aufrufen und ihre eigene Bereinigung durchführen sollte. Es lokalisiert diese Arbeit - Sie können sogar die drei Bereinigungsfunktionen sparen. Zum Teil hängt es auch davon ab, ob eine der Setup- oder Bereinigungsfunktionen in einer anderen Aufrufsequenz verwendet wird (verwendet werden kann).
Jonathan Leffler

6
Leider funktioniert dies nicht mit Schleifen - wenn ein Fehler innerhalb einer Schleife auftritt, ist ein goto viel, viel sauberer als die Alternative, Flags und 'break'-Anweisungen zu setzen und zu überprüfen (die nur geschickt getarnte gotos sind).
Adam Rosenfield

1
@ Smith, eher wie ohne kugelsichere Weste fahren.
Strager

5
Ich weiß, dass ich hier nekromantiere, aber ich finde diesen Rat ziemlich schlecht - das Pfeil-Anti-Muster sollte vermieden werden.
KingRadical

16

Ich bin überrascht, dass niemand diese Alternative vorgeschlagen hat. Obwohl die Frage schon eine Weile besteht, füge ich sie hinzu: Eine gute Möglichkeit, dieses Problem zu beheben, besteht darin, Variablen zu verwenden, um den aktuellen Status zu verfolgen. Dies ist eine Technik, die verwendet werden kann, unabhängig davon, ob gotozum Auffinden des Bereinigungscodes verwendet wird oder nicht . Wie jede Codierungstechnik hat sie Vor- und Nachteile und ist nicht für jede Situation geeignet. Wenn Sie sich jedoch für einen Stil entscheiden, sollten Sie darüber nachdenken - insbesondere, wenn Sie vermeiden möchten, gotoohne tief verschachtelte ifs zu erhalten.

Die Grundidee ist, dass es für jede Bereinigungsaktion, die möglicherweise ausgeführt werden muss, eine Variable gibt, anhand deren Wert wir erkennen können, ob die Bereinigung durchgeführt werden muss oder nicht.

Ich werde zuerst die gotoVersion zeigen , da sie näher am Code in der ursprünglichen Frage liegt.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Ein Vorteil gegenüber einigen anderen Techniken besteht darin, dass, wenn die Reihenfolge der Initialisierungsfunktionen geändert wird, die korrekte Bereinigung weiterhin erfolgt - beispielsweise unter Verwendung der switchin einer anderen Antwort beschriebenen Methode, wenn sich die Reihenfolge der Initialisierung ändert, dann die switchmuss sehr sorgfältig bearbeitet werden, um zu vermeiden, dass versucht wird, etwas zu bereinigen, das überhaupt nicht initialisiert wurde.

Einige mögen nun argumentieren, dass diese Methode eine ganze Reihe zusätzlicher Variablen hinzufügt - und in diesem Fall ist dies tatsächlich der Fall -, aber in der Praxis verfolgt eine vorhandene Variable häufig bereits den erforderlichen Zustand oder kann dazu gebracht werden, diesen zu verfolgen. Wenn dies beispielsweise prepare_stuff()tatsächlich ein Aufruf von malloc()oder an ist open(), kann die Variable verwendet werden, die den zurückgegebenen Zeiger oder Dateideskriptor enthält - zum Beispiel:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

Wenn wir nun zusätzlich den Fehlerstatus mit einer Variablen verfolgen, können wir dies gotovollständig vermeiden und trotzdem korrekt bereinigen, ohne dass Einrückungen vorhanden sind, die immer tiefer werden, je mehr Initialisierung wir benötigen:

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Auch hier gibt es mögliche Kritikpunkte:

  • Verletzen nicht all diese "Wenn" die Leistung? Nein - denn im Erfolgsfall müssen Sie sowieso alle Überprüfungen durchführen (andernfalls überprüfen Sie nicht alle Fehlerfälle). und im if (oksofar)Fehlerfall optimieren die meisten Compiler die Reihenfolge der fehlgeschlagenen Überprüfungen auf einen einzigen Sprung zum Bereinigungscode (GCC sicherlich) - und in jedem Fall ist der Fehlerfall normalerweise weniger kritisch für die Leistung.
  • Fügt dies nicht noch eine weitere Variable hinzu? In diesem Fall ja, aber oft kann die return_valueVariable verwendet werden, um die Rolle zu spielen, die oksofarhier spielt. Wenn Sie Ihre Funktionen so strukturieren, dass Fehler konsistent zurückgegeben werden, können Sie sogar die zweite ifin jedem Fall vermeiden :

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }

    Einer der Vorteile einer solchen Codierung besteht darin, dass die Konsistenz bedeutet, dass jeder Ort, an dem der ursprüngliche Programmierer vergessen hat, den Rückgabewert zu überprüfen, wie ein schmerzender Daumen hervorsteht, was das Auffinden (dieser einen Klasse von) Fehlern erheblich erleichtert.

Also - dies ist (noch) ein weiterer Stil, mit dem dieses Problem gelöst werden kann. Bei richtiger Anwendung ermöglicht es sehr sauberen, konsistenten Code - und wie bei jeder Technik kann es in den falschen Händen dazu führen, dass Code erzeugt wird, der langwierig und verwirrend ist :-)


2
Sieht so aus, als wären Sie zu spät zur Party, aber die Antwort gefällt mir auf jeden Fall!

Linus würde wahrscheinlich Ihren Code blogs.oracle.com/oswald/entry/is_goto_the_root_of
Fizz

1
@ user3588161: Wenn er würde, ist das sein Vorrecht - aber ich bin mir nicht sicher, basierend auf dem Artikel, auf den Sie verlinkt haben, dass dies der Fall ist: Beachten Sie, dass in dem Stil, den ich beschreibe, (1) die Bedingungen nicht verschachtelt sind willkürlich tief, und (2) es gibt keine zusätzlichen "if" -Anweisungen im Vergleich zu dem, was Sie sowieso benötigen (vorausgesetzt, Sie werden alle Rückkehrcodes überprüfen).
Psmears

Soviel dazu anstelle des schrecklichen Goto und der noch schlimmeren Pfeil-Antimuster-Lösung!
Das paramagnetische Croissant

8

Das Problem mit dem gotoSchlüsselwort wird meistens missverstanden. Es ist nicht einfach böse. Sie müssen sich nur der zusätzlichen Steuerpfade bewusst sein, die Sie mit jedem goto erstellen. Es wird schwierig, über Ihren Code und damit seine Gültigkeit nachzudenken.

FWIW, wenn Sie die Tutorials zu developer.apple.com nachschlagen, gehen sie bei der Fehlerbehandlung auf die gleiche Weise vor.

Wir verwenden keine gotos. Eine höhere Bedeutung wird den Rückgabewerten beigemessen. Die Ausnahmebehandlung erfolgt über setjmp/longjmp- was auch immer Sie können.


8
Während ich in einigen Fällen, in denen es erforderlich war, sicherlich setjmp / longjmp verwendet habe, würde ich es als noch "schlimmer" betrachten als goto (was ich auch etwas weniger zurückhaltend verwende, wenn es erforderlich ist). Ich verwende setjmp / longjmp nur dann, wenn entweder (1) das Ziel alles auf eine Weise herunterfährt, die von seinem aktuellen Status nicht beeinflusst wird, oder (2) das Ziel alles, was darin kontrolliert wird, neu initialisiert der setjmp / longjmp-geschützte Block in einer Weise, die von seinem gegenwärtigen Zustand unabhängig ist.
Supercat

4

An der goto-Aussage ist moralisch nichts mehr falsch als an (nichtigen) * Zeigern etwas moralisch Falsches.

Alles hängt davon ab, wie Sie das Tool verwenden. In dem von Ihnen vorgestellten (trivialen) Fall kann eine case-Anweisung dieselbe Logik erzielen, allerdings mit mehr Aufwand. Die eigentliche Frage ist: "Was ist meine Geschwindigkeitsanforderung?"

goto ist einfach nur schnell, besonders wenn Sie darauf achten, dass es zu einem kurzen Sprung kompiliert wird. Perfekt für Anwendungen, bei denen Geschwindigkeit an erster Stelle steht. Für andere Anwendungen ist es wahrscheinlich sinnvoll, den Overhead-Treffer mit if / else + case zu nehmen, um die Wartbarkeit zu gewährleisten.

Denken Sie daran: goto tötet keine Anwendungen, Entwickler töten Anwendungen.

UPDATE: Hier ist das Fallbeispiel

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 

1
Könnten Sie die Alternative "Fall" vorstellen? Ich würde auch argumentieren, dass dies ganz anders ist als void *, das für jede ernsthafte Datenstrukturabstraktion in C erforderlich ist. Ich glaube, es gibt niemanden, der sich ernsthaft gegen void * ausspricht, und ohne das Sie keine einzige große Codebasis finden werden es.
Eli Bendersky

Re: void *, das ist genau mein Punkt, es ist auch moralisch nichts falsch. Schalter / Fallbeispiel unten. int foo (int bar) {int return_value = 0; int fail_value = 0; if (! do_something (bar)) {fail_value = 1; } else if (! init_stuff (bar)) {fail_value = 2; } else if (prepare_stuff (bar)) {{return_value = do_the_thing (bar); cleanup_3 (); } switch (error_value) {case 2: cleanup_2 (); brechen ; Fall 1: cleanup_1 (); brechen ; Standard: break; }}
Webmarc

5
@webmarc, sorry aber das ist schrecklich! Sie haben goto nur vollständig mit Labels simuliert - Sie haben Ihre eigenen nicht beschreibenden Werte für Labels erfunden und goto mit switch / case implementiert. Failure_value = 1 ist sauberer als "goto cleanup_something"?
Eli Bendersky

4
Ich habe das Gefühl, Sie haben mich hier eingerichtet ... Ihre Frage ist eine Meinung und was ich bevorzugen würde. Doch als ich meine Antwort anbot, ist es schrecklich. :-( Ihre Beschwerde über die Labelnamen ist genauso aussagekräftig wie der Rest Ihres Beispiels: cleanup_1, foo, bar. Warum greifen Sie Labelnamen an, wenn dies für die Frage nicht einmal von
Belang ist

1
Ich hatte nicht die Absicht, mich "einzurichten" und irgendwelche negativen Gefühle hervorzurufen, tut mir leid! Es fühlt sich einfach so an, als ob Ihr neuer Ansatz ausschließlich auf das Entfernen von Informationen abzielt, ohne weitere Klarheit zu schaffen. Es ist, als hätten Sie neu implementiert, was goto tut, indem Sie einfach mehr Code verwenden, der weniger klar ist. Dieses IMHO ist keine gute Sache - das goto nur deswegen loszuwerden.
Eli Bendersky

2

GOTO ist nützlich. Dies kann Ihr Prozessor und deshalb sollten Sie Zugriff darauf haben.

Manchmal möchten Sie Ihrer Funktion etwas hinzufügen, und mit einem einzigen Schritt können Sie dies ganz einfach tun. Es kann Zeit sparen ..


3
Sie benötigen nicht Zugriff auf alle Funktionen Ihres Prozessors. Meistens ist goto verwirrender als die Alternative.
David Thornley

@DavidThornley: Ja, Sie tun Notwendigkeit , den Zugang zu jedem einzelnen , was Ihr Prozessor tun können, andernfalls Sie Ihren Prozessor zu verschwenden. Springen ist die beste Anleitung zum Programmieren.
Ron Maimon

1

Im Allgemeinen würde ich die Tatsache, dass ein Teil des Codes am klarsten geschrieben werden könnte, gotoals Symptom dafür betrachten, dass der Programmablauf wahrscheinlich komplizierter ist als allgemein wünschenswert. Die Kombination anderer Programmstrukturen auf seltsame Weise, um die Verwendung von zu vermeiden, gotowürde versuchen, das Symptom und nicht die Krankheit zu behandeln. Ihr spezielles Beispiel ist möglicherweise nicht allzu schwierig zu implementieren, ohne goto:

  machen {
    .. richten Sie thing1 ein, das nur bei vorzeitigem Beenden bereinigt werden muss
    if (Fehler) break;
    machen
    {
      .. richten Sie thing2 ein, das bei einem vorzeitigen Beenden bereinigt werden muss
      if (Fehler) break;
      // ***** SIEHE TEXT ZU DIESER LINIE
    } while (0);
    .. Aufräumen thing2;
  } while (0);
  .. Bereinigungssache1;

Wenn die Bereinigung jedoch nur stattfinden sollte, wenn die Funktion fehlschlug, konnte der gotoFall behandelt werden, indem ein returnkurz vor dem ersten Zieletikett gesetzt wurde. Der obige Code würde das Hinzufügen eines returnin der mit gekennzeichneten Zeile erfordern *****.

Im Szenario "Bereinigen auch im Normalfall" würde ich die Verwendung gotoals klarer als die do/ while(0)-Konstrukte betrachten, unter anderem, weil die Zielbezeichnungen selbst praktisch "LOOK AT ME" schreien, weitaus mehr als die breakund do/ -Konstrukte while(0). Für den Fall "Nur bei Fehler bereinigen" muss sich die returnAnweisung unter dem Gesichtspunkt der Lesbarkeit an der schlechtesten Stelle befinden (Rückgabeanweisungen sollten im Allgemeinen entweder am Anfang einer Funktion stehen oder wie "aussieht"). das Ende); Ein returnkurz vor einem Zieletikett zu haben, erfüllt diese Qualifikation viel leichter als ein kurz vor dem Ende einer "Schleife".

Übrigens, ein Szenario, das ich manchmal gotozur Fehlerbehandlung verwende, befindet sich in einer switchAnweisung, wenn der Code für mehrere Fälle denselben Fehlercode aufweist. Obwohl mein Compiler oft klug genug ist, um zu erkennen, dass mehrere Fälle mit demselben Code enden, ist es meiner Meinung nach klarer zu sagen:

 REPARSE_PACKET:
  Schalter (Paket [0])
  {
    case PKT_THIS_OPERATION:
      if (Problembedingung)
        gehe zu PACKET_ERROR;
      ... mit THIS_OPERATION umgehen
      brechen;
    case PKT_THAT_OPERATION:
      if (Problembedingung)
        gehe zu PACKET_ERROR;
      ... mit THAT_OPERATION umgehen
      brechen;
    ...
    case PKT_PROCESS_CONDITIONALLY
      if (packet_length <9)
        gehe zu PACKET_ERROR;
      if (packet_condition mit packet [4])
      {
        Paketlänge - = 5;
        memmove (Paket, Paket + 5, Paketlänge);
        gehe zu REPARSE_PACKET;
      }}
      sonst
      {
        Paket [0] = PKT_CONDITION_SKIPPED;
        Paket [4] = Paketlänge;
        Paketlänge = 5;
        packet_status = READY_TO_SEND;
      }}
      brechen;
    ...
    Standard:
    {
     PACKET_ERROR:
      packet_error_count ++;
      Paketlänge = 4;
      Paket [0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      brechen;
    }}
  }}   

Obwohl man die gotoAnweisungen durch ersetzen könnte {handle_error(); break;}und obwohl man eine do/ while(0)Schleife zusammen mit verwenden könnte continue, um das umschlossene Paket mit bedingter Ausführung zu verarbeiten, denke ich nicht, dass dies klarer ist als die Verwendung von a goto. Ferner kann , während es den Code abschreiben könnte möglich sein , von PACKET_ERRORüberall goto PACKET_ERRORverwendet wird, und während ein Compiler einmal , um den duplizierten Code schreiben könnte und die meisten Vorkommen mit einem Sprung auf die freigegebenen Kopie ersetzen, die mit der gotoes einfacher zu bemerken Orten macht die das Paket etwas anders einrichten (z. B. wenn der Befehl "Bedingt ausführen" beschließt, nicht auszuführen).


1

Ich persönlich bin ein Anhänger der "The Power of Ten - 10 Regeln für das Schreiben von sicherheitskritischem Code" .

Ich werde einen kleinen Ausschnitt aus diesem Text hinzufügen, der zeigt, was ich für eine gute Idee zu goto halte.


Regel: Beschränken Sie den gesamten Code auf sehr einfache Kontrollflusskonstrukte. Verwenden Sie keine goto-Anweisungen, setjmp- oder longjmp-Konstrukte und keine direkte oder indirekte Rekursion.

Begründung: Ein einfacherer Kontrollfluss führt zu stärkeren Überprüfungsmöglichkeiten und führt häufig zu einer verbesserten Codeklarheit. Die Verbannung der Rekursion ist hier vielleicht die größte Überraschung. Ohne Rekursion haben wir jedoch garantiert einen azyklischen Funktionsaufrufgraphen, der von Codeanalysatoren ausgenutzt werden kann und direkt dazu beitragen kann, zu beweisen, dass alle Ausführungen, die begrenzt werden sollten, tatsächlich begrenzt sind. (Beachten Sie, dass diese Regel nicht erfordert, dass alle Funktionen einen einzigen Rückgabepunkt haben - obwohl dies häufig auch den Kontrollfluss vereinfacht. Es gibt jedoch genügend Fälle, in denen eine frühzeitige Fehlerrückgabe die einfachere Lösung ist.)


Die Verwendung von goto zu verbannen scheint schlecht, aber:

Wenn die Regeln auf den ersten Blick drakonisch erscheinen, denken Sie daran, dass sie es ermöglichen sollen, Code zu überprüfen, bei dem Ihr Leben buchstäblich von seiner Richtigkeit abhängt: Code, der zur Steuerung des Flugzeugs verwendet wird, mit dem Sie fliegen, des Kernkraftwerks ein paar Meilen von Ihrem Wohnort entfernt oder das Raumschiff, das Astronauten in die Umlaufbahn bringt. Die Regeln wirken wie der Sicherheitsgurt in Ihrem Auto: Anfangs sind sie vielleicht etwas unangenehm, aber nach einer Weile wird ihre Verwendung zur Selbstverständlichkeit und ihre Nichtbenutzung wird unvorstellbar.


22
Das Problem dabei ist, dass die übliche Art, das vollständig zu verbannen, darin gotobesteht, eine Reihe von „cleveren“ Booleschen Werten in tief verschachtelten ifs oder Schleifen zu verwenden. Das hilft wirklich nicht. Vielleicht werden Ihre Werkzeuge es besser machen, aber Sie werden es nicht und Sie sind wichtiger.
Donal Fellows

1

Ich bin damit einverstanden, dass die in der Frage angegebene umgekehrte Reihenfolge in den meisten Funktionen die sauberste Methode zum Aufräumen ist. Aber ich wollte auch darauf hinweisen, dass Sie manchmal möchten, dass Ihre Funktion trotzdem aufgeräumt wird. In diesen Fällen verwende ich die folgende Variante, wenn if (0) {label:} idiom, um zum richtigen Punkt des Bereinigungsprozesses zu gelangen:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }

0

Mir scheint, das cleanup_3sollte seine Bereinigung machen, dann anrufen cleanup_2. In ähnlicher Weise cleanup_2sollte es bereinigen und dann cleanup_1 aufrufen. Es scheint , dass , wann immer Sie tun cleanup_[n], die cleanup_[n-1]erforderlich ist, so sollte es in der Verantwortung des Verfahrens sein (so dass zum Beispiel cleanup_3kann nie ohne Aufruf aufgerufen werden cleanup_2und möglicherweise verursacht ein Leck.)

Bei diesem Ansatz würden Sie anstelle von gotos einfach die Bereinigungsroutine aufrufen und dann zurückkehren.

Der gotoAnsatz ist jedoch nicht falsch oder schlecht , es ist nur erwähnenswert, dass es nicht unbedingt der "sauberste" Ansatz (IMHO) ist.

Wenn Sie nach der optimalen Leistung suchen, ist die gotoLösung wahrscheinlich die beste. Ich erwarte jedoch nur, dass es in einigen wenigen leistungskritischen Anwendungen (z. B. Gerätetreibern, eingebetteten Geräten usw.) relevant ist. Andernfalls handelt es sich um eine Mikrooptimierung, die eine niedrigere Priorität als die Klarheit des Codes hat.


4
Das wird nicht schaden - Bereinigungen sind spezifisch für Ressourcen, die nur in dieser Routine in dieser Reihenfolge zugewiesen werden. An anderen Orten sind sie nicht verwandt, daher macht es keinen Sinn, sich gegenseitig anzurufen.
Eli Bendersky

0

Ich denke, dass die Frage hier in Bezug auf den gegebenen Code trügerisch ist.

Erwägen:

  1. do_something (), init_stuff () und prepare_stuff () scheinen zu wissen, ob sie fehlgeschlagen sind, da sie in diesem Fall entweder false oder nil zurückgeben.
  2. Die Verantwortung für das Einrichten des Status scheint in der Verantwortung dieser Funktionen zu liegen, da in foo () kein Status direkt eingerichtet wird.

Deshalb: do_something (), init_stuff () und prepare_stuff () sollten ihre eigene Bereinigung durchführen . Eine separate Funktion cleanup_1 (), die nach do_something () bereinigt wird, verstößt gegen die Philosophie der Kapselung. Es ist schlechtes Design.

Wenn sie selbst bereinigt haben, wird foo () ganz einfach.

Andererseits. Wenn foo () tatsächlich einen eigenen Status erstellt hätte, der abgerissen werden müsste, wäre goto angemessen.


0

Folgendes habe ich bevorzugt:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}

0

Alte Diskussion jedoch ... was ist mit der Verwendung von "Pfeil-Anti-Muster" und dem späteren Einkapseln jeder verschachtelten Ebene in eine statische Inline-Funktion? Der Code sieht sauber aus, ist optimal (wenn Optimierungen aktiviert sind) und es wird kein goto verwendet. Kurz gesagt, teilen und erobern. Unten ein Beispiel:

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

In Bezug auf den Speicherplatz erstellen wir die dreifache Variable im Stapel, was nicht gut ist. Dies verschwindet jedoch, wenn beim Kompilieren mit -O2 die Variable aus dem Stapel entfernt und in diesem einfachen Beispiel ein Register verwendet wird. Was ich aus dem obigen Block bekommen habe, gcc -S -O2 test.cwar das Folgende:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols

-1

Ich bevorzuge die im folgenden Beispiel beschriebene Technik ...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}}

Quelle: http://blog.staila.com/?p=114


2
Flaggy Code und das Pfeil-Anti-Pattern (beide in Ihrem Beispiel dargestellt) sind beide Dinge, die den Code unnötig komplizieren. Es gibt keine Rechtfertigung außerhalb von "goto is evil", sie zu benutzen.
KingRadical

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.