it-swarm.com.de

Verwenden Sie GOTO oder nicht?

Momentan arbeite ich an einem Projekt, in dem goto-Statements häufig verwendet werden. Der Hauptzweck von goto-Anweisungen besteht darin, einen Reinigungsabschnitt in einer Routine zu haben, anstatt mehrere return-Anweisungen zu verwenden.

BOOL foo()
{
   BOOL bRetVal = FALSE;
   int *p = NULL;

   p = new int;
   if (p == NULL)
   {
     cout<<" OOM \n";
     goto Exit;
   }

   // Lot of code...

Exit:
   if(p)
   {
     delete p;
     p = NULL;
   }
   return bRetVal;
}

Dies macht es viel einfacher, da wir unseren Bereinigungscode in einem Abschnitt des Codes nachverfolgen können, dh nach dem Beenden-Label.

Ich habe jedoch an vielen Stellen gelesen, dass es schlecht ist, Aussagen zu machen.

Derzeit lese ich das Buch Code Complete, und es heißt, dass wir Variablen verwenden müssen, die nahe an ihren Deklarationen liegen. Wenn wir goto verwenden, müssen wir alle Variablen vor der ersten Verwendung von goto deklarieren/initialisieren. Andernfalls gibt der Compiler Fehler an, dass die Initialisierung der xx-Variablen von der goto-Anweisung übersprungen wird.

Welcher Weg ist richtig?


Aus Scott's Kommentar:

Die Verwendung von goto zum Springen von einem Abschnitt zum anderen ist schlecht, da der Code schwer lesbar und verständlich ist.

Aber wenn wir goto verwenden, um nur zu einem Label zu gelangen, dann sollte es in Ordnung sein (?).

55
anand

Ich bin nicht sicher, was Sie mit Bereinigungscode meinen, aber in C++ gibt es ein Konzept namens "Ressourcenerfassung ist Initialisierung", und es sollte in der Verantwortung Ihrer Destruktoren liegen, Dinge zu bereinigen.

(Beachten Sie, dass dies in C # und Java normalerweise durch try/finally gelöst wird.)

Weitere Informationen finden Sie auf dieser Seite: http://www.research.att.com/~bs/bs_faq2.html#finally

EDIT: Lassen Sie mich das ein wenig klären.

Betrachten Sie den folgenden Code:

void MyMethod()
{
    MyClass *myInstance = new MyClass("myParameter");
    /* Your code here */
    delete myInstance;
}

Das Problem: Was passiert, wenn Sie mehrere Funktionen der Funktion haben? Sie müssen jeden Ausgang nachverfolgen und Ihre Objekte an allen möglichen Ausgängen löschen! Sonst haben Sie Speicherlecks und Zombie-Ressourcen, richtig?

Die Lösung: Verwenden Sie stattdessen Objektreferenzen, da diese automatisch bereinigt werden, wenn das Steuerelement den Bereich verlässt.

void MyMethod()
{
    MyClass myInstance("myParameter");
    /* Your code here */
    /* You don't need delete - myInstance will be destructed and deleted
     * automatically on function exit */
}

Oh ja, und benutze std::unique_ptr oder etwas Ähnliches, weil das obige Beispiel offensichtlich unvollständig ist.

58
Tamas Czinege

In C++ musste ich noch nie einen goto verwenden. Je. JE. Wenn es eine Situation gibt, in der es verwendet werden sollte, ist es unglaublich selten. Wenn Sie erwägen, goto zu einem Standardbestandteil Ihrer Logik zu machen, ist etwas aus den Fugen geraten.

59
Gene Roberts

Grundsätzlich gibt es zwei Punkte, die die Leute in Bezug auf Gotos und Ihren Code ansprechen: 

  1. Goto ist schlecht. Es ist sehr selten, dass Sie einen Ort finden, an dem Sie Gotos brauchen, aber ich würde nicht empfehlen, ihn komplett zu schlagen. Obwohl C++ über einen ausreichend intelligenten Kontrollfluss verfügt, ist es selten sinnvoll, dies zu tun. 

  2. Dein Mechanismus für die Bereinigung ist falsch: Dieser Punkt ist viel wichtiger. In C ist die eigene Speicherverwaltung nicht nur in Ordnung, sondern häufig auch die beste Vorgehensweise. In C++ sollte es das Ziel sein, die Speicherverwaltung so weit wie möglich zu vermeiden. Sie sollten die Speicherverwaltung so weit wie möglich vermeiden. Lassen Sie den Compiler das für Sie tun. Statt new zu verwenden, deklarieren Sie einfach Variablen. Sie benötigen die Speicherverwaltung nur dann, wenn Sie die Größe Ihrer Daten nicht im Voraus wissen. Selbst dann sollten Sie versuchen, stattdessen nur einige der STL-Sammlungen zu verwenden.

Falls Sie rechtmäßig eine Speicherverwaltung benötigen (Sie haben nicht wirklich Beweise dafür vorgelegt), sollten Sie Ihre Speicherverwaltung über Konstruktoren in einer Klasse kapseln, um Speicher und Dekonstruktoren zuzuweisen, um Speicher freizugeben.

Ihre Antwort, dass Ihre Arbeitsweise viel einfacher ist, ist auf lange Sicht nicht wirklich wahr. Erstens, sobald Sie ein starkes Gefühl für C++ haben, werden solche Konstruktoren zur zweiten Natur. Persönlich finde ich die Verwendung von Konstruktoren einfacher als die Verwendung von Bereinigungscode, da ich nicht unbedingt darauf achten muss, dass ich die Zuordnung richtig freigebe. Stattdessen kann ich einfach zulassen, dass das Objekt den Gültigkeitsbereich verlässt und die Sprache es für mich erledigt. Außerdem ist das WARTEN VIEL einfacher als das Aufrechterhalten eines Bereinigungsabschnitts und weniger anfällig für Probleme.

Kurz gesagt, kann goto in einigen Situationen eine gute Wahl sein, aber nicht in dieser. Hier ist es nur kurzfristige Faulheit.

22
Brian

Ihr Code ist nicht idiomatisch und Sie sollten ihn niemals schreiben. Sie emulieren im Wesentlichen C in C++. Andere haben jedoch darauf hingewiesen und auf RAII als Alternative verwiesen.

Ihr Code funktioniert nicht wie erwartet, denn Folgendes gilt:

p = new int;
if(p==NULL) { … }

wird ever nicht zu true ausgewertet (es sei denn, Sie haben operator new auf seltsame Weise überladen). Wenn operator new nicht genügend Speicherplatz zuordnen kann, gibt es eine Ausnahme aus. Never, ever gibt 0 zurück, zumindest nicht mit diesem Parametersatz. Es gibt eine spezielle Überladung für die Platzierung - eine neue Instanz des Typs std::nothrow, die 0 zurückgibt, anstatt eine Ausnahme auszulösen. Diese Version wird jedoch im normalen Code selten verwendet. Einige Low-Level-Codes oder Embedded-Device-Anwendungen könnten davon profitieren, wenn der Umgang mit Ausnahmen zu teuer ist.

Ähnliches gilt für Ihren delete-Block, wie Harald gesagt hat: if (p) ist vor delete p nicht erforderlich.

Ich bin mir auch nicht sicher, ob Ihr Beispiel absichtlich gewählt wurde, da dieser Code wie folgt umgeschrieben werden kann:

bool foo() // prefer native types to BOOL, if possible
{
    bool ret = false;
    int i;
    // Lots of code.
    return ret;
}
20
Konrad Rudolph

Wahrscheinlich keine gute Idee .

16

Im Allgemeinen und an der Oberfläche gibt es nichts Falsches an Ihrem Ansatz, vorausgesetzt, Sie haben nur ein Label und die Gotos gehen immer weiter. Zum Beispiel dieser Code:

int foo()
{
    int *pWhatEver = ...;
    if (something(pWhatEver))
    { 
        delete pWhatEver;
        return 1;
    }
    else
    {
        delete pWhatEver;
        return 5;
    }
}

Und dieser Code:

int foo()
{
    int ret;
    int *pWhatEver = ...;
    if (something(pWhatEver))
    { 
        ret = 1;
        goto exit;
    }
    else
    {
        ret = 1;
        goto exit;
    }
exit:
    delete pWhatEver;
    return ret;
}

sind wirklich nicht so verschieden voneinander. Wenn Sie eines akzeptieren können, sollten Sie das andere akzeptieren können.

In vielen Fällen kann jedoch das Muster RAII (Ressourcenerfassung ist Initialisierung) den Code viel sauberer und wartbarer machen. Zum Beispiel dieser Code:

int foo()
{
    Auto<int> pWhatEver = ...;

    if (something(pWhatEver))
    {
        return 1;
    }
    else
    {
        return 5;
    }
}

ist kürzer, einfacher zu lesen und einfacher zu warten als die beiden vorherigen Beispiele.

Ich würde also empfehlen, den RAII-Ansatz zu verwenden, wenn Sie können.

11

Ich denke, andere Antworten (und ihre Kommentare) haben alle wichtigen Punkte abgedeckt, aber hier ist eine Sache, die noch nicht richtig gemacht wurde:

Wie sollte Ihr Code stattdessen aussehen:

bool foo() //lowercase bool is a built-in C++ type. Use it if you're writing C++.
{
  try {
    std::unique_ptr<int> p(new int);
    // lots of code, and just return true or false directly when you're done
  }
  catch (std::bad_alloc){ // new throws an exception on OOM, it doesn't return NULL
    cout<<" OOM \n";
    return false;
  }
}

Nun, es ist kürzer und, soweit ich sehen kann, korrekter (behandelt den OOM-Fall ordnungsgemäß), und vor allem musste ich keinen Bereinigungscode schreiben oder etwas Besonderes tun, um sicherzustellen, dass mein Rückgabewert initialisiert wird ".

Ein Problem mit Ihrem Code, das mir erst beim Schreiben aufgefallen ist, ist "Was zum Teufel ist der Wert von bRetVal an diesem Punkt?". Ich weiß nicht, weil es oben als waaaaay deklariert wurde und wann es zuletzt zugewiesen wurde? Irgendwann darüber. Ich muss die gesamte Funktion durchlesen, um sicherzustellen, dass ich verstehe, was zurückgegeben wird.

Und wie kann ich mich davon überzeugen, dass die Erinnerung frei wird?

Wie kann ich wissen, dass wir nie vergessen, zum Bereinigungsetikett zu springen? Ich muss vom Bereinigungsetikett aus rückwärts arbeiten und every goto finden, das darauf hinweist, und, was noch wichtiger ist, diejenigen finden, die nicht da sind. Ich muss alle Pfade der Funktion nachverfolgen, um sicherzugehen, dass die Funktion ordnungsgemäß bereinigt wird. Das liest sich für mich wie Spaghetti-Code.

Sehr fragiler Code, da jedes Mal, wenn eine Ressource bereinigt werden muss, nicht vergessen Ihr Bereinigungscode dupliziert werden muss. Warum nicht einmal in den Typ schreiben, der aufgeräumt werden muss? Und dann darauf vertrauen, dass es jedes Mal automatisch ausgeführt wird, wenn wir es brauchen?

8
jalf

Ihr Beispiel ist nicht ausnahmesicher.

Wenn Sie mit goto den Code bereinigen, wird eine Ausnahme vor dem Bereinigungscode vollständig übersehen. Wenn Sie behaupten, dass Sie keine Ausnahmen verwenden, werden Sie sich irren, da die Variable new bad_alloc auslöst, wenn nicht genügend Speicher vorhanden ist.

Auch an diesem Punkt (wenn Bad_alloc ausgelöst wird) wird Ihr Stack abgewickelt, wobei der Bereinigungscode in jeder Funktion auf dem Weg des Aufrufstapels fehlt, sodass der Code nicht bereinigt wird.

Sie müssen einige Nachforschungen zu intelligenten Zeigern durchführen. In der obigen Situation können Sie einfach einen std::auto_ptr<> verwenden.

Beachten Sie auch, dass in C++ - Code nicht geprüft werden muss, ob ein Zeiger NULL ist (normalerweise, weil Sie nie RAW-Zeiger haben), sondern weil new nicht NULL zurückgibt (es wird ausgelöst).

Im Gegensatz zu (C) ist es in C++ üblich, frühe Rückgaben im Code zu sehen. Dies liegt daran, dass RAII die Bereinigung automatisch durchführt, während Sie im C-Code sicherstellen müssen, dass Sie am Ende der Funktion einen speziellen Bereinigungscode hinzufügen (ein bisschen wie Ihr Code).

8
Martin York

Sie sollten diese Thread-Zusammenfassung aus den Linux-Kernel-Mailinglisten lesen (wobei Sie besonders auf die Antworten von Linus Torvalds achten sollten), bevor Sie eine Richtlinie für goto erstellen:

http://kerneltrap.org/node/553/2131

6
too much php

In den acht Jahren, in denen ich programmiert habe, habe ich viel verwendet, meistens im ersten Jahr, als ich eine Version von GW-BASIC und ein Buch von 1980 verwendete, das es nicht geschafft hat clear goto sollte nur in bestimmten Fällen verwendet werden. Das einzige Mal, dass ich goto in C++ verwendet habe, war, als ich den folgenden Code hatte, und ich bin mir nicht sicher, ob es einen besseren Weg gab.

for (int i=0; i<10; i++) {
    for (int j=0; j<10; j++)
    {
        if (somecondition==true)
        {
            goto finish;
        }
        //Some code
    }
    //Some code
}
finish:

Die einzige Situation, die mir bekannt ist, wo goto noch stark verwendet wird, ist die Mainframe-Assembly-Sprache, und die Programmierer, die ich kenne, stellen sicher, dass der Code springt und warum.

6
Jared

Wie im Linux-Kernel verwendet, funktionieren gotos für die Bereinigung gut, wenn eine einzelne Funktion zwei oder mehr Schritte ausführen muss, die möglicherweise rückgängig gemacht werden müssen. Schritte müssen keine Speicherzuordnung sein. Es kann sich um eine Konfigurationsänderung eines Codes oder eines Registers eines E/A-Chipsatzes handeln. Goto's sollten nur in wenigen Fällen benötigt werden, aber bei richtiger Anwendung können sie die beste Lösung von best sein. Sie sind nicht böse. Sie sind ein Werkzeug.

Anstatt...

do_step1;
if (failed)
{
  undo_step1;
  return failure;
}

do_step2;
if (failed)
{
  undo_step2;
  undo_step1;
  return failure;
}

do_step3;
if (failed)
{
  undo_step3;
  undo_step2;
  undo_step1;
  return failure;
}

return success;

sie können dasselbe mit goto-Statements wie folgt machen:

do_step1;
if (failed) goto unwind_step1;

do_step2;
if (failed) goto unwind_step2;

do_step3;
if (failed) goto unwind_step3;

return success;

unwind_step3:
  undo_step3;

unwind_step2:
  undo_step2;

unwind_step1:
  undo_step1;

return failure;

Es sollte klar sein, dass angesichts dieser beiden Beispiele eines der anderen vorzuziehen ist. Was das RAII-Publikum angeht ... Es ist nichts falsch mit diesem Ansatz, solange sie garantieren können, dass das Abwickeln immer in genau umgekehrter Reihenfolge erfolgt: 3, 2, 1. Und schließlich verwenden einige Leute keine Ausnahmen in ihrem Code und weisen Sie die Compiler an, sie zu deaktivieren. Daher muss nicht jeder Code ausnahmesicher sein.

6
Harvey

Im Allgemeinen sollten Sie Ihre Programme so gestalten, dass der Bedarf an gotos begrenzt wird. Verwenden Sie OO -Techniken für die "Bereinigung" Ihrer Rückgabewerte. Es gibt Möglichkeiten, dies zu tun, ohne dass man Gotos verwenden oder den Code komplizieren muss. Es gibt Fälle, in denen gotos sehr nützlich sind (z. B. tief verschachtelte Bereiche), aber wenn möglich vermieden werden sollten.

5
Marcin

Der Nachteil von GOTO wird ziemlich gut diskutiert. Ich füge nur hinzu: 1) Manchmal müssen Sie sie verwenden und sollten wissen, wie Sie die Probleme minimieren können, und 2) einige akzeptierte Programmiertechniken sind GOTO-in-Verkleidung.

1) Wenn Sie GOTO verwenden müssen, z. B. in ASM- oder .bat-Dateien, denken Sie wie ein Compiler. Wenn Sie codieren möchten

 if (some_test){
  ... the body ...
}

tun, was ein Compiler macht Generieren Sie ein Etikett, dessen Zweck es ist, den Körper zu überspringen, und nicht das zu tun, was folgt. d.h.

 if (not some_test) GOTO label_at_end_of_body
  ... the body ...
label_at_end_of_body:

Nicht

 if (not some_test) GOTO the_label_named_for_whatever_gets_done_next
  ... the body ...

the_label_named_for_whatever_gets_done_next:

Mit anderen Worten, der Zweck des Labels besteht nicht darin, do etwas zu tun, sondern - zu überspringen etwas.

2) Was ich als GOTO-in-disguise bezeichne, ist alles, was durch die Definition einiger Makros in GOTO + LABELS-Code umgewandelt werden kann. Ein Beispiel ist die Technik zum Implementieren von Finite-State-Automaten, indem eine Zustandsvariable und eine while-switch-Anweisung verwendet werden.

 while (not_done){
    switch(state){
        case S1:
            ... do stuff 1 ...
            state = S2;
            break;
        case S2:
            ... do stuff 2 ...
            state = S1;
            break;
        .........
    }
}

kann sich verwandeln in:

 while (not_done){
    switch(state){
        LABEL(S1):
            ... do stuff 1 ...
            GOTO(S2);
        LABEL(S2):
            ... do stuff 2 ...
            GOTO(S1);
        .........
    }
}

nur durch die Definition einiger Makros. So gut wie jede FSA kann in strukturierten Code umgewandelt werden. Ich ziehe es vor, mich von GOTO-in-disguise-Code fernzuhalten, da er zu den gleichen Spaghetti-Code-Problemen führen kann wie nicht verdeckte gotos.

Hinzugefügt: Nur um zu beruhigen: Ich denke, ein Kennzeichen eines guten Programmierers besteht darin, zu erkennen, dass die allgemeinen Regeln nicht zutreffen.

5
Mike Dunlavey

Goto bietet bessere Wiederholungen (DRY) nicht, wenn "Endlogik" in einigen, aber nicht allen Fällen üblich ist. Besonders in einer "switch" -Anweisung verwende ich häufig gotos, wenn einige der Switch-Zweige gemeinsame Endpunkte haben.

switch(){
   case a:  ... goto L_abTail;
   case b: ... goto L_abTail;
L_abTail: <commmon stuff>
    break://end of case b
case c:
.....
}//switch

Sie haben wahrscheinlich bemerkt, dass die Einführung zusätzlicher geschweiften Klammern ausreicht, um den Compiler zu befriedigen, wenn Sie eine solche Tail-End-Verschmelzung in der Mitte einer Routine benötigen. Mit anderen Worten, Sie müssen nicht alles ganz oben erklären. das ist in der Tat eine schlechte Lesbarkeit.

...
   goto L_skipMiddle;
{
    int declInMiddleVar = 0;
    ....
}
L_skipMiddle: ;

Mit den späteren Versionen von Visual Studio , die die Verwendung nicht initialisierter Variablen erkennen, initialisiere ich die meisten Variablen, obwohl ich denke, dass sie in allen Zweigen zugewiesen werden können. Es ist leicht, eine "tracing" -Anweisung zu codieren, die eine Variable referenziert Dies wurde nie zugewiesen, da Ihr Verstand die Ablaufverfolgungsaussage nicht als "echten Code" betrachtet, aber Visual Studio erkennt natürlich immer noch einen Fehler.

Abgesehen davon, dass Sie sich nicht wiederholen, scheint das Zuweisen von Markennamen zu einer solchen End-Logik sogar meinem Geist zu helfen, die Dinge klar zu halten, indem Sie Nice-Markennamen wählen. Ohne ein aussagekräftiges Etikett könnten Ihre Kommentare das gleiche sagen.

Wenn Sie tatsächlich Ressourcen zuweisen, müssen Sie, wenn auto-ptr nicht passt, wirklich einen try-catch verwenden, aber Tail-End-Merge-Don't-Repeat-Yourself passiert häufig, wenn die Ausnahmesicherheit besteht kein Problem.

Zusammenfassend lässt sich sagen, dass goto zwar zum Codieren von Spaghetti-artigen Strukturen verwendet werden kann, im Fall einer Tail-End-Sequenz, die in einigen, aber nicht allen Fällen üblich ist, VERBESSERT der goto die Lesbarkeit des Codes und sogar die Wartbarkeit Wenn Sie sonst Sachen kopieren/einfügen würden, so dass jemand später das eine und das andere nicht aktualisieren kann. Es ist also ein weiterer Fall, in dem Fanatismus über ein Dogma kontraproduktiv sein kann.

5
pngaz

Die Verwendung von "goto" zu einem Aufräumabschnitt führt zu vielen Problemen.

Erstens sind Bereinigungsabschnitte anfällig für Probleme. Sie haben eine geringe Kohäsion (keine reale Rolle, die in Bezug auf das Programm beschrieben werden kann), eine hohe Kopplung (die Korrektheit hängt sehr stark von anderen Codeabschnitten ab) und sind überhaupt nicht ausnahmesicher. Prüfen Sie, ob Sie Destruktoren für die Bereinigung verwenden können. Wenn beispielsweise int *p in auto_ptr<int> p geändert wird, wird automatisch veröffentlicht, auf was p zeigt.

Zweitens werden Sie, wie Sie darauf hinweisen, dazu gezwungen sein, Variablen lange vor der Verwendung zu deklarieren, was das Verständnis des Codes erschwert.

Drittens, während Sie eine ziemlich disziplinierte Anwendung von goto vorschlagen, wird es die Versuchung geben, sie auf eine lockerere Art und Weise zu verwenden, und dann wird der Code schwer verständlich.

Es gibt sehr wenige Situationen, in denen ein goto sinnvoll ist. Die meiste Zeit, wenn Sie versucht sind, sie zu verwenden, ist dies ein Signal, dass Sie Dinge falsch machen.

4
David Thornley

Da dies ein klassisches Thema ist, werde ich mit der Go-to-Anweisung von Dijkstra als schädlich (ursprünglich in ACM veröffentlicht) antworten.

4
mstrobl

Der Zweck der Funktion "Every-function-has-a-single-exit-point" in C bestand darin, das gesamte Bereinigungsmaterial an einem einzigen Ort abzulegen. Wenn Sie C++ - Destruktoren für die Bereinigung verwenden, ist dies nicht mehr erforderlich. Die Bereinigung wird unabhängig von der Anzahl der Exitpunkte einer Funktion ausgeführt. In richtig entworfenem C++ - Code besteht also kein Bedarf mehr.

3
Head Geek

Die einzigen zwei Gründe, aus denen ich goto in meinem C++ - Code verwende, sind:

  • Verschachtelte Loops der Stufe 2+ brechen
  • Komplizierte Flüsse wie dieser (ein Kommentar in meinem Programm):

    /* Analysis algorithm:
    
      1.  if classData [exporter] [classDef with name 'className'] exists, return it,
          else
      2.    if project/target_codename/temp/classmeta/className.xml exist, parse it and go back to 1 as it will succeed.
      3.    if that file don't exists, generate it via haxe -xml, and go back to 1 as it will succeed.
    
    */
    

Für die Lesbarkeit des Codes hier habe ich nach diesem Kommentar das Label step1 definiert und in Schritt 2 und 3 verwendet. Tatsächlich sind in über 60 Quelldateien nur diese Situation und eine verschachtelte 4-Ebene die Orte, an denen ich goto verwendet habe. Nur zwei Orte.

Viele Leute, die mit gotos ausflippen, sind böse; Sie sind nicht. Das heißt, Sie werden niemals einen brauchen; Es gibt immer einen besseren Weg.

Wenn ich finde, dass ich einen goto "brauche", um diese Art von Dingen zu tun, finde ich fast immer, dass mein Code zu komplex ist und leicht in einige Methodenaufrufe zerlegt werden kann, die einfacher zu lesen und zu handhaben sind. Ihr Anrufcode kann Folgendes tun:

// Setup
if(
     methodA() &&
     methodB() &&
     methodC()
 )
 // Cleanup

Das ist zwar nicht perfekt, aber es ist viel einfacher zu befolgen, da alle Ihre Methoden benannt werden, um deutlich zu machen, was das Problem sein könnte.

Wenn Sie die Kommentare durchlesen, sollte dies jedoch darauf hindeuten, dass Ihr Team dringlichere Probleme hat als der Umgang mit dem Programm.

3
Bill K

Der Code, den Sie uns geben, ist (fast) C-Code, der in einer C++ - Datei geschrieben ist. Die Art der Speicherbereinigung, die Sie verwenden, wäre in einem C-Programm, das keinen C++ - Code Bibliotheken verwendet, in Ordnung.

In C++ ist Ihr Code einfach unsicher und unzuverlässig. In C++ wird die Art der Verwaltung, nach der Sie fragen, unterschiedlich ausgeführt. Konstruktoren/Destruktoren verwenden. Verwenden Sie intelligente Zeiger. Verwenden Sie den Stapel. Verwenden Sie in einem Wort/- _ raii _ .

Ihr Code könnte (d. H. In C++, SOLLTE) folgendermaßen geschrieben werden:

BOOL foo()
{
   BOOL bRetVal = FALSE;

   std::auto_ptr<int> p = new int;

   // Lot of code...

   return bRetVal ;
}

(Beachten Sie, dass das Neuladen eines int in realem Code etwas dumm ist, aber Sie können int durch ein beliebiges Objekt ersetzen, und dann ist es sinnvoller. Stellen wir uns vor, wir haben ein Objekt vom Typ T (T könnte ein int, einige C++ - Klasse usw. sein). Dann wird der Code zu:

BOOL foo()
{
   BOOL bRetVal = FALSE;

   std::auto_ptr<T> p = new T;

   // Lot of code...

   return bRetVal ;
}

Oder noch besser mit dem Stack:

BOOL foo()
{
   BOOL bRetVal = FALSE;

   T p ;

   // Lot of code...

   return bRetVal;
}

In jedem der oben genannten Beispiele sind Größen besser lesbar und sicherer als in Ihrem Beispiel.

RAII hat viele Facetten (d. H. Die Verwendung von intelligenten Zeigern, des Stapels, der Verwendung von Vektoren anstelle von Arrays mit variabler Länge usw.), aber insgesamt geht es darum, so wenig Code wie möglich zu schreiben und den Compiler im richtigen Moment aufräumen zu lassen.

2
paercebal

Ich werde nicht sagen, dass goto immer schlecht ist, aber Ihre Verwendung ist es sicherlich. Diese Art von "Bereinigungsabschnitten" war in den frühen 1990er Jahren ziemlich üblich, aber die Verwendung für neuen Code ist reines Übel.

1

Dieser Code hat eine Reihe von Problemen, von denen die meisten bereits genannt wurden, zum Beispiel:

  • Die Funktion ist zu lang; Das Umgestalten von Code in separate Funktionen kann hilfreich sein.

  • Die Verwendung von Zeigern bei normalen Instanzen funktioniert wahrscheinlich gut.

  • STL Typen wie auto_ptr nicht nutzen

  • Fehlerhaftes Prüfen auf Fehler, Ausnahmen werden nicht erkannt. (Ich würde der Meinung sein, dass das Überprüfen auf OOM auf der großen Mehrheit der Plattformen nicht sinnvoll ist, da bei zu wenig Arbeitsspeicher größere Probleme auftreten, als durch die Software behoben werden können, es sei denn, Sie schreiben das Betriebssystem selbst.)

Ich habe noch nie einen goto-Befehl benötigt, und ich habe immer festgestellt, dass die Verwendung von goto ein Symptom für eine größere Anzahl von Problemen ist. Ihr Fall scheint keine Ausnahme zu sein.

1
metao

Der einfachste Weg, um zu vermeiden, was Sie hier tun, besteht darin, all diese Bereinigung in eine Art einfache Struktur zu bringen und eine Instanz davon zu erstellen. Zum Beispiel statt:

void MyClass::myFunction()
{
   A* a = new A;
   B* b = new B;
   C* c = new C;
   StartSomeBackgroundTask();
   MaybeBeginAnUndoBlockToo();

   if ( ... )
   {
     goto Exit;
   }

   if ( ... ) { .. }
   else
   {
      ... // what happens if this throws an exception??? too bad...
      goto Exit;
   }

Exit:
  delete a;
  delete b;
  delete c;
  StopMyBackgroundTask();
  EndMyUndoBlock();
}

sie sollten diese Bereinigung lieber wie folgt durchführen:

struct MyFunctionResourceGuard
{
  MyFunctionResourceGuard( MyClass& owner ) 
  : m_owner( owner )
  , _a( new A )
  , _b( new B )
  , _c( new C )
  {
      m_owner.StartSomeBackgroundTask();
      m_owner.MaybeBeginAnUndoBlockToo();
  }

  ~MyFunctionResourceGuard()
  {
     m_owner.StopMyBackgroundTask();
     m_owner.EndMyUndoBlock();
  }

  std::auto_ptr<A> _a;
  std::auto_ptr<B> _b;
  std::auto_ptr<C> _c;

};

void MyClass::myFunction()
{
   MyFunctionResourceGuard guard( *this );

   if ( ... )
   {
     return;
   }

   if ( ... ) { .. }
   else
   {
      ...
   }
}
1
Michel

Ich denke, die Verwendung des goto für Exit-Code ist schlecht, da es viele andere Lösungen mit geringem Overhead gibt, beispielsweise eine Exit-Funktion und bei Bedarf den Exit-Funktionswert zurückgeben. Normalerweise sollte dies jedoch in Member-Funktionen nicht erforderlich sein, da dies ansonsten darauf hindeuten könnte, dass etwas zu viel Code-Bloat auftritt.

Normalerweise ist die einzige Ausnahme, die ich bei der Programmierung der Regel "no goto" mache, das Aufbrechen verschachtelter Schleifen auf ein bestimmtes Niveau, was bei der mathematischen Programmierung nur erforderlich war.

Zum Beispiel:

for(int i_index = start_index; i_index >= 0; --i_index)
{
    for(int j_index = start_index; j_index >=0; --j_index)
        for(int k_index = start_index; k_index >= 0; --k_index)
            if(my_condition)
                goto BREAK_NESTED_LOOP_j_index;
BREAK_NESTED_LOOP_j_index:;
}
1
Hazok

All das oben Gesagte ist gültig. Vielleicht möchten Sie auch prüfen, ob Sie die Komplexität Ihres Codes reduzieren und die Notwendigkeit von gotos verringern können, indem Sie die Menge des Codes reduzieren, der in dem Abschnitt mit "viel Code" markiert ist. in deinem Beispiel. Zusätzlich ist delete 0 eine gültige C++ - Anweisung

1

Die Verwendung von GOTO-Labels in C++ ist eine schlechte Methode zum Programmieren. Sie können den Bedarf verringern, indem Sie OO-Programmierung (Dekonstruktoren!) Ausführen und versuchen Prozeduren so klein wie möglich halten.

Ihr Beispiel sieht ein bisschen seltsam aus, da gibt es keine Notwendigkeit, einen NULL-Zeiger zu löschen. Und heutzutage wird eine Ausnahme ausgelöst, wenn ein Zeiger nicht zugeordnet werden kann.

Ihre Prozedur könnte einfach so geschrieben werden:

bool foo()
{
    bool bRetVal = false;
    int p = 0;

    // Calls to various methods that do algorithms on the p integer
    // and give a return value back to this procedure.

    return bRetVal;
}

Sie sollten einen try catch-Block in das Hauptprogramm einfügen, der Probleme mit zu wenig Arbeitsspeicher behandelt und den Benutzer über Speichermangel, der sehr selten vorkommt ​​... informiert. (Informiert das Betriebssystem selbst nicht darüber.) auch?)

Beachten Sie auch, dass nicht immer ein Zeiger verwendet werden muss, sondern nur für dynamische Dinge. (Das Erstellen einer Sache in einer Methode, die nicht von der Eingabe von irgendwoher abhängt, ist nicht wirklich dynamisch.)

1
Tamara Wijsman

Vor ein paar Jahren habe ich mir ein Pseudo-Idiom ausgedacht, das goto vermeidet und der Ausnahmebehandlung in C etwas ähnelt. Wahrscheinlich wurde es bereits von jemand anderem erfunden, also denke ich, ich habe es "unabhängig entdeckt" :)

BOOL foo()
{
   BOOL bRetVal = FALSE;
   int *p=NULL;

   do
   {
       p = new int;
       if(p==NULL)
       {
          cout<<" OOM \n";
          break;
       }

       // Lot of code...

       bRetVal = TRUE;

    } while (false);

   if(p)
   {
     delete p;
     p= NULL;
   }

   return bRetVal;
}
1
ggambett

Ich habe vielleicht etwas verpasst: Sie springen zum Label Exit, wenn P null ist, und testen Sie dann, ob es nicht null ist (was nicht der Fall ist), um zu sehen, ob Sie es löschen müssen (was nicht unbedingt erforderlich ist, da es nie zugewiesen wurde der erste Ort).

Das if/goto funktioniert nicht und muss nicht gelöscht werden. Das Ersetzen des goto durch ein return false hätte den gleichen Effekt (und Sie könnten dann das Exit-Label entfernen). 

Die einzigen Orte, an denen ich weiß, wo goto's nützlich sind, sind tief in unangenehmen Parsern (oder lexikalischen Analysatoren) und beim Ausblenden von Zustandsmaschinen (in einer Masse von CPP-Makros begraben). In diesen beiden Fällen wurden sie verwendet, um eine sehr verdrehte Logik zu vereinfachen. Dies ist jedoch sehr selten.

Funktionen (A ruft A '), Try/Catches und setjmp/longjmps sind alles andere, um ein schwieriges Syntaxproblem zu vermeiden.

Paul.

0
Paul W Homer

Ignorieren Sie die Tatsache, dass new niemals NULL zurückgibt. Nehmen Sie Ihren Code:

  BOOL foo()
  {
     BOOL bRetVal = FALSE;

     int *p=NULL;

     p = new int;

     if(p==NULL)
     {
        cout<<" OOM \n";
        goto Exit;
     }

     // Lot of code...

  Exit:
     if(p)
     {
        delete p;
        p= NULL;
     }

     return bRetVal;
  }

und schreib es so:

  BOOL foo()
  {
     BOOL bRetVal = FALSE;

     int *p = new int;

     if (p!=NULL)
     {
        // Lot of code...

        delete p;
     }
     else
     {
        cout<<" OOM \n";
     }

     return bRetVal;
  }
0
jussij

Versuchen Sie es so:

BOOL foo()
{
   BOOL bRetVal = FALSE;
   int *p = NULL;

   p = new int;
   if (p == NULL)
   {
     cout<<" OOM \n";
   }
   else
   {
       // Lot of code...
   }

   if (p)
   {
     delete p;
     p = NULL;
   }
   return bRetVal;
}

Im Abschnitt "Menge Code" ist "Menge Code" ein guter Hinweis darauf, dass Sie diesen Abschnitt wahrscheinlich in eine oder mehrere Methoden oder Funktionen umwandeln sollten.

0
Matthew

Wenn Sie "GOTO" verwenden, werden die "Logiken" eines Programms und die Art und Weise, wie Sie die Interpretation vornehmen oder wie Sie sich vorstellen, dass es funktionieren würde, geändert.

Das Vermeiden von GOTO-Befehlen hat immer für mich funktioniert. Wenn Sie denken, dass Sie es vielleicht brauchen, brauchen Sie nur ein Re-Design.

Wenn wir uns dies jedoch auf einer Assmebly-Ebene ansehen, ist das "Springen" von juse wie die Verwendung von GOTO, und das wird ständig verwendet, ABER, in Assembly können Sie herausfinden, was Sie auf dem Stack und anderen Registern vor sich haben weitergeben.

Wenn Sie also GOTO verwenden, würde ich sicherstellen, dass die Software "erscheinen" würde, während die Co-Codierer die Interpretation vornehmen würden. GOTO hat einen "schlechten" Effekt auf Ihre Software imho.

Das ist mehr eine Erklärung dafür, warum man GOTO nicht verwendet und keine Lösung für einen Ersatz, denn das hängt sehr davon ab, wie alles andere aufgebaut ist.

0
Filip Ekberg

Die vorherigen Kommentare sind alles gute Gründe, um goto nicht zu verwenden.

Ich kann aus Erfahrung sagen, dass für andere Programmierer, die möglicherweise Ihre Code-Gotos beibehalten müssen, die Logik sehr hart folgt. Ich kam in eine Situation, in der es zu heftigen gotischen Spaghetti-Codes kam, und vor meinem OO Hintergrund war es ein Albtraum zum Debuggen und Ändern. Ja, dieser Code verwendete gotos auch für die Bereinigungsfunktionen. Sehr frustrierend, wenn nicht benötigt. Verwenden Sie keine goto's, es sei denn, es ist absolut notwendig.

0
Jeff Schmidt

Alien01 hat geschrieben: Derzeit arbeite ich an einem Projekt, in dem goto-Statements häufig verwendet werden. Der Hauptzweck von goto-Anweisungen besteht darin, einen Reinigungsabschnitt in der Routine zu haben, statt mehrere return-Anweisungen.

Mit anderen Worten, Sie möchten die Programmlogik von einfachen, sich wiederholenden langwierigen Routinen trennen, z. B. indem Sie eine Ressource freigeben, die möglicherweise an verschiedenen Stellen des Codes reserviert ist.

Die Ausnahmebehandlungstechnik ist eine Fehlerbehandlungslogik, die parallel zur Programmlogik arbeitet. Dies ist eine elegantere Lösung, da sie eine solche Trennung bietet und gleichzeitig die Möglichkeit bietet, die Kontrolle auf andere Codeblöcke genau wie die Anweisung goto zu verschieben. Daher habe ich Ihr Skript so geändert, dass es wie folgt aussieht:

class auxNullPtrException : public std::exception {
    public:
        auxNullPtrException::auxNullPtrException()
            : std::exception( " OOM \n") {}
    };

    BOOL foo()
    {
        BOOL bRetVal = FALSE;
        try {
            int *p = NULL;
            p = new int;
            if (p == NULL)
            {
                throw auxNullPtrException();
            }
            // Lot of code...
         }
         catch(auxNullPtrException & _auxNullPtrException)
         {
             std::cerr<<_auxNullPtrException.what();
             if(p)
             {
                 delete p;
                 p = NULL;
             }
         }
         return bRetVal;
    }
0
Josef

Aus allen vorherigen Kommentaren:

  1. goto ist sehr sehr schlecht
  2. Es macht den Code schwer zu lesen und zu verstehen.
  3. Es kann das bekannte Problem "Spaghetti-Code" verursachen
  4. Alle sind sich einig, dass dies nicht getan werden sollte.

Aber ich verwende in folgendem Szenario

  1. Es wird verwendet, um vorwärts und nur zu einem Label zu gehen.
  2. der goto-Abschnitt wird zum Bereinigen von Code und zum Festlegen eines Rückgabewerts verwendet. Wenn ich goto nicht verwende, muss ich für jeden Datentyp eine Klasse erstellen. Als ob ich int * in eine Klasse packen muss.
  3. Es wird im ganzen Projekt verfolgt.

Ich stimme zu, dass es schlecht ist, aber es macht die Dinge viel einfacher, wenn man es richtig befolgt.

0
anand