Czy to jest dobry przypadek użycia goto w C?

59

Naprawdę waham się zadać to pytanie, ponieważ nie chcę „zabiegać o debatę, argumenty, ankiety lub rozszerzoną dyskusję”, ale jestem nowy w C i chcę uzyskać lepszy wgląd w typowe wzorce stosowane w języku.

Niedawno usłyszałem niechęć do tego gotopolecenia, ale ostatnio również znalazłem dla niego przyzwoity przypadek użycia.

Kod taki jak ten:

error = function_that_could_fail_1();
if (!error) {
    error = function_that_could_fail_2();
    if (!error) {
        error = function_that_could_fail_3();
        ...to the n-th tab level!
    } else {
        // deal with error, clean up, and return error code
    }
} else {
    // deal with error, clean up, and return error code
}

Jeśli część dotycząca oczyszczania jest bardzo podobna, można ją napisać trochę ładniej (moja opinia?) W ten sposób:

error = function_that_could_fail_1();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
    goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code

Czy jest to powszechny lub dopuszczalny przypadek użycia gotow C? Czy istnieje inny / lepszy sposób na zrobienie tego?

Robz
źródło
3
Zobacz także to pytanie (i moja odpowiedź ).
Keith Thompson,
2
Kompilator zapewnia więcej niż powlekanie cukru, ale gwarantuje czyszczenie i sprawdzanie błędów, podczas gdy w C musisz to zrobić ręcznie, naruszając DRY i gwarantując, że w jakimś miejscu byłeś zbyt leniwy, aby sprawdzić kod błędu lub poszedł na niewłaściwą etykietę lub coś w tym rodzaju.
DeadMG
3
@Izkata: Nie, chodzi o to, aby mieć funkcje (takie jak destruktory), które są domyślnie wywoływane przez kompilator, gdy zostanie zgłoszony wyjątek.
DeadMG
6
@DeadMG: Używanie innego języka nie zawsze jest opcją.
Benjamin Kloster
13
Myślę, że przy odpowiedniej nazwie etykiety, przykład może wyglądać całkiem przyzwoicie:goto hell;
gnat

Odpowiedzi:

48

gotoOświadczenie (oraz odpowiadające mu etykiety) to kontrola przepływu prymitywny (wraz z warunkowym wykonaniem instrukcji). Rozumiem przez to, że są tam, aby umożliwić ci budowę sieci kontroli przepływu programu. Możesz myśleć o nich jak o modelowaniu strzałek między węzłami schematu blokowego.

Niektóre z nich można natychmiast zoptymalizować, gdy występuje bezpośredni przepływ liniowy (wystarczy użyć sekwencji podstawowych instrukcji). Inne wzorce najlepiej zastępować strukturalnymi strukturami programowania, jeśli są one dostępne; jeśli wygląda jak whilepętla, użyj whilepętli , OK? Strukturyzowane wzorce programowania są zdecydowanie potencjalnie jaśniejsze niż zamiar gotoinstrukcji.

Jednak C nie obejmuje wszystkich możliwych strukturalnych struktur programowania. (Nie jest dla mnie jasne, że wszystkie istotne zostały już odkryte; tempo odkrycia jest teraz wolne, ale wahałbym się powiedzieć, że wszystkie zostały znalezione.) Z tych, o których wiemy, C zdecydowanie nie ma try/ catch/ finallystructure (i wyjątki też). Brakuje również wielopoziomowej breakpętli. Są to rzeczy, które gotomożna zastosować do wdrożenia. Do tego celu można również użyć innych schematów - wiemy, że C ma wystarczający zestawgotoprymitywy - ale często wiążą się z tworzeniem zmiennych flagowych i znacznie bardziej złożonych warunków pętli lub ochrony; zwiększenie uwikłania analizy kontrolnej w analizę danych sprawia, że ​​program jest trudniejszy do zrozumienia. Utrudnia to również kompilatorowi optymalizację i procesorowi szybkie wykonywanie (większość konstrukcji kontroli przepływu - i zdecydowanie goto - jest bardzo tania).

Tak więc, chociaż nie powinieneś używać, gotochyba że jest to konieczne, powinieneś zdawać sobie sprawę, że istnieje i że może być potrzebny, a jeśli potrzebujesz, nie powinieneś czuć się źle. Przykładem przypadku, w którym jest to potrzebne, jest dezalokacja zasobów, gdy wywoływana funkcja zwraca błąd. (To znaczy try/ finally.) Można to napisać bez gotozrobienia tego, co może mieć swoje wady, takie jak problemy z utrzymaniem go. Przykład sprawy:

int frobnicateTheThings() {
    char *workingBuffer = malloc(...);
    int i;

    for (i=0 ; i<numberOfThings ; i++) {
        if (giveMeThing(i, workingBuffer) != OK)
            goto error;
        if (processThing(workingBuffer) != OK)
            goto error;
        if (dispatchThing(i, workingBuffer) != OK)
            goto error;
    }

    free(workingBuffer);
    return OK;

  error:
    free(workingBuffer);
    return OOPS;
}

Kod może być jeszcze krótszy, ale wystarcza, aby to wykazać.

Donal Fellows
źródło
4
+1: W C goto technicznie nigdy nie jest „potrzebny” - zawsze istnieje sposób, aby to zrobić, robi się bałagan ..... Aby uzyskać solidny zestaw wytycznych dotyczących korzystania z goto, zobacz MISRA C.
mattnz
1
Wolisz try/catch/finally, aby gotopomimo podobnego jeszcze bardziej wszechobecnej (jak może rozprzestrzeniać całej wielu funkcji / modułów) postaci kodu spaghetti, które jest możliwe przy użyciu try/catch/finally?
autystyczny
65

Tak.

Jest używany na przykład w jądrze Linuksa. Oto e-mail z końca wątku sprzed prawie dekady , śmiały mój:

Od: Robert Love
Temat: Re: czy jest jakaś szansa na test 2.6.0 *?
Data: 12 stycznia 2003 17:58:06 -0500

On Sun, 12.01.2003 o 17:22, Rob Wilkens napisał:

Mówię „proszę nie używaj goto”, a zamiast tego mam funkcję „cleanup_lock” i dodaję to przed wszystkimi instrukcjami zwrotnymi. Nie powinno to stanowić obciążenia. Tak, wymaga od programisty trochę cięższej pracy, ale efektem końcowym jest lepszy kod.

Nie, jest obrzydliwy i powoduje wzdęcie jądra . Wprowadza mnóstwo śmieci dla N ścieżek błędów, w przeciwieństwie do kodu wyjścia raz na końcu. Ślad pamięci podręcznej jest kluczem i właśnie go zabiłeś.

Nie jest też łatwiejsze do odczytania.

Jako ostatni argument, nie pozwala nam to robić czystego, typowego dla stosu wiatru i odpoczywać , tj

        do A
        if (error)
            goto out_a;
        do B
        if (error)
            goto out_b;
        do C
        if (error)
            goto out_c;
        goto out;
        out_c:
        undo C
        out_b:
        undo B:
        out_a:
        undo A
        out:
        return ret;

Teraz przestań.

Robert Love

To powiedziawszy, wymaga dużo dyscypliny, aby powstrzymać się od tworzenia kodu spaghetti, gdy przyzwyczaisz się do używania goto, więc jeśli nie piszesz czegoś, co wymaga szybkości i niskiego poziomu pamięci (jak jądro lub system osadzony), powinieneś naprawdę pomyśl o tym, zanim napiszesz pierwsze goto.

Izkata
źródło
21
Zauważ, że jądro różni się od programu innego niż jądro pod względem priorytetu prędkości surowej w porównaniu z czytelnością. Innymi słowy, już JUŻ profilowali i stwierdzili, że muszą zoptymalizować swój kod pod kątem szybkości z goto.
11
Używanie un-wind stosu do obsługi czyszczenia po błędzie bez faktycznego wciskania na stos! To niesamowite wykorzystanie goto.
mike30
1
@ user1249, Śmieci, nie możesz profilować każdej {przeszłej, istniejącej, przyszłej} aplikacji używającej fragmentu kodu {biblioteka, jądro}. Po prostu musisz być szybki.
Pacerier
1
Niepowiązane: Jestem zdumiony, jak ludzie mogą korzystać z list adresowych, aby cokolwiek zrobić, nie mówiąc już o tak ogromnych projektach. To takie ... prymitywne. Jak ludzie wchodzą do straży pożarnej wiadomości ?!
Alexander
2
Nie martwię się tak bardzo o umiar. Jeśli ktoś jest na tyle miękki, aby zostać odwrócony przez jakiegoś dupka w Internecie, prawdopodobnie lepiej jest bez niego projekt. Bardziej martwię się niepraktycznością nadążania za zaporą przychodzących wiadomości i tym, jak na przykład możesz mieć naturalną, zacofaną dyskusję przy tak małej liczbie narzędzi do śledzenia cytatów.
Alexander
14

Moim zdaniem opublikowany kod jest przykładem prawidłowego użycia goto, ponieważ przeskakujesz tylko w dół i używasz go tylko jako prymitywnej procedury obsługi wyjątków.

Jednak ze względu na starą debatę goto, programiści unikali gotood około 40 lat i dlatego nie są przyzwyczajeni do czytania kodu za pomocą goto. Jest to ważny powód, aby unikać goto: po prostu nie jest to standard.

Przepisałbym kod jako coś łatwiejszego do odczytania przez programistów C:

Error some_func (void)
{
  Error error;
  type_t* resource = malloc(...);

  error = some_other_func (resource);

  free (resource);

  /* error handling can be placed here, or it can be returned to the caller */

  return error;
}


Error some_other_func (type_t* resource)  // inline if needed
{
  error = function_that_could_fail_1();
  if(error)
  {
    return error;
  }

  /* ... */

  error = function_that_could_fail_2();
  if(error)
  {
    return error;
  }

  /* ... */

  return ok;
}

Zalety tego projektu:

  • Funkcja wykonująca rzeczywistą pracę nie musi zajmować się zadaniami nieistotnymi dla jej algorytmu, takimi jak przydzielanie danych.
  • Kod będzie wyglądał mniej obco dla programistów C, ponieważ boją się goto i etykiet.
  • Możesz scentralizować obsługę błędów i dezalokację w tym samym miejscu, poza funkcją wykonującą algorytm. Funkcja nie ma sensu, aby funkcja obsługiwała własne wyniki.
Doktor Brown
źródło
9

W Javie zrobiłbyś to tak:

makeCalls:  {
    error = function_that_could_fail_1();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_2();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_3();
    if (error) {
        break makeCalls;
    }
    ...
    return 0;  // No error code.
}
// deal with error if it exists, clean up
// return error code

Często tego używam. Mimo że nie lubię gotojęzyka, w większości innych języków w stylu C używam twojego kodu; nie ma innego dobrego sposobu na zrobienie tego. (Wyskakiwanie z zagnieżdżonych pętli jest podobnym przypadkiem; w Javie używam etykiet breaki wszędzie indziej używam a goto.)

RalphChapin
źródło
3
Aw, to ładna struktura kontrolna.
Bryan Boettcher,
4
To jest naprawdę interesujące. Zwykle myślałem o użyciu struktury try / catch / wreszcie do tego w java (generowanie wyjątków zamiast łamania).
Robz
5
To jest naprawdę nieczytelne (przynajmniej dla mnie). Jeśli są obecne, wyjątki są znacznie lepsze.
m3th0dman
1
@ m3th0dman Zgadzam się z tobą w tym konkretnym przykładzie (obsługa błędów). Są jednak inne (nie wyjątkowe) przypadki, w których ten idiom może się przydać.
Konrad Rudolph
1
Wyjątki są drogie, muszą wygenerować błąd, stacktrace i wiele innych śmieci. Ten podział etykiety daje czyste wyjście z pętli sprawdzającej. chyba że nie zależy nam na pamięci i szybkości, wtedy obchodzi mnie wyjątek.
Tschallacka
8

Myślę, że jest to dobry przypadek użycia, ale w przypadku, gdy „błąd” jest niczym więcej niż wartością logiczną, istnieje inny sposób na osiągnięcie tego, co chcesz:

error = function_that_could_fail_1();
error = error || function_that_could_fail_2();
error = error || function_that_could_fail_3();
if(error)
{
     // do cleanup
}

Wykorzystuje to ocenę zwarciową operatorów logicznych. Jeśli to „lepsze”, zależy od twojego osobistego gustu i tego, jak jesteś przyzwyczajony do tego idiomu.

Doktor Brown
źródło
1
Problem polega na tym, że errorwartość może stać się bez znaczenia dla wszystkich OR.
James
@James: zredagowałem moją odpowiedź z powodu twojego komentarza
Doc Brown
1
To nie wystarcza. Jeśli wystąpił błąd podczas pierwszej funkcji, nie chcę wykonywać drugiej lub trzeciej funkcji.
Robz
2
Jeśli z krótkim ręki oceny znaczy zwarciem ocenę, to nie jest dokładnie to, co się robi tutaj ze względu na wykorzystanie bitowym OR zamiast logiczną OR.
Chris mówi Przywróć Monikę
1
@ChristianRau: dzięki, odpowiednio zredagowałem moją odpowiedź
Doc Brown
6

Przewodnik po stylu linuxa podaje konkretne powody, dla których warto zastosować te goto, które są zgodne z twoim przykładem:

https://www.kernel.org/doc/Documentation/process/coding-style.rst

Uzasadnieniem użycia gotos jest:

  • bezwarunkowe stwierdzenia są łatwiejsze do zrozumienia i przestrzegania
  • zagnieżdżanie jest zmniejszone
  • błędy wynikające z braku aktualizacji poszczególnych punktów wyjścia podczas wprowadzania modyfikacji
  • zapisuje pracę kompilatora w celu optymalizacji nadmiarowego kodu;)

Oświadczenie Nie powinienem udostępniać swojej pracy. Przykłady tutaj są nieco wymyślone, więc proszę o wyrozumiałość.

Jest to dobre do zarządzania pamięcią. Ostatnio pracowałem nad kodem, który dynamicznie przydzielał pamięć (na przykład char *zwracany przez funkcję). Funkcja, która patrzy na ścieżkę i sprawdza, czy ścieżka jest poprawna, analizując tokeny ścieżki:

tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        free(var1);
        free(var2);
        ...
        free(varN);
        return 1;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            free(var1);
            free(var2);
            ...
            free(varN);
            return 1;
        } else {
            free(var1);
            free(var2);
            ...
            free(varN);
            return 0;
        }
    }
    token = strtok(NULL,delim);
}

free(var1);
free(var2);
...
free(varN);
return 1;

Teraz dla mnie poniższy kod jest o wiele ładniejszy i łatwiejszy w utrzymaniu, jeśli musisz dodać varNplus1:

int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        retval = 1;
        goto out_free;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            retval = 1;
            goto out_free;
        } else {
            retval = 0;
            goto out_free;
        }
    }
    token = strtok(NULL,delim);
}

out_free:
free(var1);
free(var2);
...
free(varN);
return retval;

Teraz kod miał wiele innych problemów, mianowicie, że N był gdzieś powyżej 10, a funkcja miała ponad 450 linii, z 10 poziomami zagnieżdżenia w niektórych miejscach.

Ale zaproponowałem mojemu przełożonemu, żeby to zrefaktoryzował, co zrobiłem, a teraz jest to zbiór funkcji, które są krótkie i wszystkie mają styl linux

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 == NULL ){
        retval = 0;
        goto out;
    }

    if( isValid(var1) ){
         retval = some_function(var1);
         goto out_free;
    }

    if( isGood(var1) ){
         retval = 0;
         goto out_free;
    }

out_free:
    free(var1);
out:
    return retval;
}

Jeśli weźmiemy pod uwagę ekwiwalent bez gotos:

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 != NULL ){

       if( isValid(var1) ){
            retval = some_function(var1);
       } else {
          if( isGood(var1) ){
               retval = 0;
          }
       }
       free(var1);

    } else {
       retval = 0;
    }

    return retval;
}

Dla mnie, w pierwszym przypadku, jest dla mnie oczywiste, że jeśli pierwsza funkcja powróci NULL, wyjdziemy stąd i wrócimy 0. W drugim przypadku muszę przewinąć w dół, aby zobaczyć, czy if zawiera całą funkcję. Przyznany pierwszy wskazuje mi to stylistycznie (nazwa „ out”), a drugi robi to składniowo. Pierwszy jest jeszcze bardziej oczywisty.

Ponadto zdecydowanie wolę mieć free()instrukcje na końcu funkcji. Po części dlatego, że z mojego doświadczenia free()stwierdzenia pośrodku funkcji źle pachną i wskazują mi, że powinienem utworzyć podprogram. W tym przypadku utworzyłem var1w swojej funkcji i nie mogłem tego free()zrobić w podprogramie, ale dlatego goto out_freestyl goto out jest tak praktyczny.

Myślę, że należy wychować programistów wierzących, że gototo zło. Następnie, gdy są wystarczająco dojrzali, powinni przejrzeć kod źródłowy Linuksa i przeczytać przewodnik po stylu Linux.

Powinienem dodać, że używam tego stylu bardzo konsekwentnie, każda funkcja ma int retval, out_freeetykietę i etykietę wyjściową. Ze względu na spójność stylistyczną poprawiono czytelność.

Bonus: Łamie się i trwa

Powiedz, że masz pętlę while

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);

    if( functionC(var1, var2){
         count++
         continue;
    }

    ...
    a bunch of statements
    ...

    count++;
    free(var1);
    free(var2);
}

W tym kodzie występują inne błędy, ale jedną z nich jest instrukcja Continue. Chciałbym przepisać całość, ale miałem za zadanie zmodyfikować ją w niewielki sposób. Refaktoryzacja zajęłaby mi kilka dni w sposób, który mnie satysfakcjonuje, ale faktyczna zmiana zajęła około pół dnia pracy. Problem polega na tym, że nawet jeśli continuemy nadal musimy uwolnić var1i var2. Musiałem dodać a var3, a to sprawiło, że chciałem się zmusić, aby dublować instrukcje free ().

Byłem wówczas stosunkowo nowym stażystą, ale już dawno szukałem kodu źródłowego linuksa, więc zapytałem mojego przełożonego, czy mógłbym użyć instrukcji goto. Powiedział tak, a ja to zrobiłem:

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);
    var3 = newFunction(line,count);

    if( functionC(var1, var2){
         goto next;
    }

    ...
    a bunch of statements
    ...
next:
    count++;
    free(var1);
    free(var2);
}

Myślę, że kontynuacja jest w najlepszym razie OK, ale dla mnie są jak goto z niewidzialną etykietą. To samo dotyczy przerw. Nadal wolałbym kontynuować lub przerwać, chyba że, tak jak w tym przypadku, zmusi cię to do lustrzanych modyfikacji w wielu miejscach.

Powinienem również dodać, że to użycie goto next;i next:etykieta są dla mnie niezadowalające. Są po prostu lepsze niż odzwierciedlenie wypowiedzi free()i count++oświadczeń.

gotosą prawie zawsze w błędzie, ale trzeba wiedzieć, kiedy są dobre w użyciu.

Jedną z rzeczy, o których nie rozmawiałem, jest obsługa błędów, która została omówiona w innych odpowiedziach.

Występ

Można spojrzeć na implementację strtok () http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c

#include <stddef.h>
#include <string.h>

char *
strtok(s, delim)
    register char *s;
    register const char *delim;
{
    register char *spanp;
    register int c, sc;
    char *tok;
    static char *last;


    if (s == NULL && (s = last) == NULL)
        return (NULL);

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
        if (c == sc)
            goto cont;
    }

    if (c == 0) {       /* no non-delimiter characters */
        last = NULL;
        return (NULL);
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;) {
        c = *s++;
        spanp = (char *)delim;
        do {
            if ((sc = *spanp++) == c) {
                if (c == 0)
                    s = NULL;
                else
                    s[-1] = 0;
                last = s;
                return (tok);
            }
        } while (sc != 0);
    }
    /* NOTREACHED */
}

Popraw mnie, jeśli się mylę, ale uważam, że cont:etykieta i goto cont;oświadczenie służą zapewnieniu wydajności (z pewnością nie zwiększają czytelności kodu). W ten sposób można je zastąpić czytelnym kodem

while( isDelim(*s++,delim));

pomijać ograniczniki. Aby jednak działać jak najszybciej i unikać niepotrzebnych wywołań funkcji, robią to w ten sposób.

Przeczytałem artykuł Dijkstry i uważam go za dość ezoteryczny.

google „oświadczenie dijkstra goto uznane za szkodliwe”, ponieważ nie mam wystarczającej reputacji, aby opublikować więcej niż 2 linki.

Widziałem to jako powód, aby nie używać goto, a czytanie tego niczego nie zmieniło, o ile moje zastosowania goto są pomijane.

Dodatek :

Wymyśliłem porządną zasadę, myśląc o tym wszystkim o ciągłości i zerwaniu.

  • Jeśli w pętli while masz ciąg dalszy, wówczas treść pętli while powinna być funkcją, a kontynuacja powinna być instrukcją return.
  • Jeśli w pętli while masz instrukcję break, to sama pętla while powinna być funkcją, a break powinien stać się instrukcją return.
  • Jeśli masz oba te elementy, coś może być nie tak.

Nie zawsze jest to możliwe z powodu problemów z zakresem, ale odkryłem, że dzięki temu znacznie łatwiej jest rozumować mój kod. Zauważyłem, że za każdym razem, gdy pętla chwilowa miała przerwę lub kontynuację, wywoływało to złe przeczucie.

Philippe Carphin
źródło
2
+1, ale czy mogę się nie zgodzić w jednym punkcie? „Myślę, że trzeba wychować programistów wierzących, że goto są złe”. Być może tak, ale po raz pierwszy nauczyłem się programować w języku BASIC, z numerami linii i GOTO, bez edytora tekstu, w 1975 roku. Spotkałem się z programowaniem strukturalnym dziesięć lat później, po upływie miesiąca przestałem używać GOTO na własną rękę, bez wszelkie naciski, aby zatrzymać. Dzisiaj używam GOTO kilka razy w roku z różnych powodów, ale nie pojawia się wiele. Nie wychowanie się w przekonaniu, że GOTO jest złem, nie wyrządziło mi żadnej krzywdy, którą znam, a może nawet przyniosło jakieś korzyści. To tylko ja.
thb
1
Myślę, że masz rację. Wpadłem na pomysł, że GOTO nie powinny być używane i przez przypadek przeglądałem kod źródłowy Linuksa w czasie, gdy pracowałem nad kodem, który miał te funkcje z wieloma punktami wyjścia z pamięcią do zwolnienia. W przeciwnym razie nigdy nie wiedziałbym o tych technikach.
Philippe Carphin
1
@thb Ponadto, zabawna historia, zapytałem wówczas swojego przełożonego jako stażysty o pozwolenie na korzystanie z GOTO i upewniłem się, że wyjaśniłem mu, że zamierzam ich używać w określony sposób, na przykład w sposób, w jaki jest używany w Jądro Linuksa powiedział: „Ok, to ma sens, a także nie wiedziałem, że możesz używać GOTO w C”.
Philippe Carphin
1
@thb Nie wiem, czy dobrze jest przejść do pętli (zamiast przerywać pętle) jak ta ? Cóż, jest to zły przykład, ale uważam, że szybkie sortowanie z instrukcjami goto (przykład 7a) w programowaniu strukturalnym Knuth z przejściem do instrukcji jest niezrozumiałe.
Yai0Phah
@ Yai0Phah Wyjaśnię mój punkt, ale moje wyjaśnienie nie umniejsza twojego pięknego przykładu 7a! Popieram przykład. Mimo to szefowie drugiego roku lubią wykładać ludziom o goto. Trudno znaleźć praktyczne zastosowanie goto od 1985 roku, które powoduje znaczne problemy, podczas gdy można znaleźć nieszkodliwe goto, które ułatwiają pracę programisty. W każdym razie tak rzadko pojawia się we współczesnym programowaniu, że kiedy się pojawi, moja rada jest taka, że ​​jeśli chcesz go użyć, prawdopodobnie powinieneś go po prostu użyć. Goto jest w porządku. Głównym problemem związanym z goto jest to, że niektórzy uważają, że wycofanie goto sprawia, że ​​wyglądają elegancko.
thb
5

Osobiście przerobiłbym to bardziej tak:

int DoLotsOfStuffThatCouldFail (paramstruct *params)
{
    int errcode = EC_NOERROR;

    if ((errcode = FunctionThatCouldFail1 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail2 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail3 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail4 (params)) != EC_NOERROR) return errcode;

    return EC_NOERROR;
}

void DoStuff (paramstruct *params)
{
    int errcode = EC_NOERROR;

    InitStuffThatMayNeedToBeCleaned (params);

    if ((errcode = DoLotsOfStuffThatCouldFail (params)) != EC_NOERROR)
    {
         CleanupAfterError (params, errcode);
    }
}

Byłoby to bardziej motywowane przez unikanie głębokiego zagnieżdżania niż unikanie goto (IMO gorszy problem z pierwszą próbką kodu) i oczywiście zależałoby od tego, czy CleanupAfterError jest możliwy poza zakresem (w tym przypadku „parametry” mogą być strukturą zawierającą przydzieloną pamięć, którą musisz zwolnić, PLIK *, który musisz zamknąć lub cokolwiek innego.

Jedną z głównych zalet, które widzę w tym podejściu, jest to, że zarówno łatwiejsze, jak i czystsze jest wybranie hipotetycznego przyszłego dodatkowego kroku między, powiedzmy, FTCF2 i FTCF3 (lub usunięcie istniejącego bieżącego kroku), więc lepiej nadaje się do utrzymania (i osoby, która dziedziczy mój kod, który nie chce mnie zlinczować!) - na bok, w wersji zagnieżdżonej brakuje tego.

Maximus Minimus
źródło
1
Nie podałem tego w moim pytaniu, ale możliwe jest, że FTCF NIE mają tych samych parametrów, co czyni ten wzór nieco bardziej skomplikowanym. W każdym razie dzięki.
Robz
3

Spójrz na wytyczne kodowania C MISRA (Motor Industry Software Reliability Association), które pozwalają na goto pod ścisłymi kryteriami (które spełnia Twój przykład)

Tam, gdzie pracuję, napisany byłby ten sam kod - nie ma potrzeby. - Unikanie niepotrzebnej debaty religijnej na ich temat jest dużym plusem w każdym domu oprogramowania.

error = function_that_could_fail_1();
if(!error) {
  error = function_that_could_fail_2();
}
if(!error) {
  error = function_that_could_fail_3();
} 
if(!error) {
...
if (error) {
  cleanup:
} 

lub „goto in drag” - coś jeszcze bardziej podejrzanego niż goto, ale omija „No goto Ever !!!” obóz) „Na pewno musi być OK, nie używa Goto” ....

do {
  if (error = function_that_could_fail_1() ){
    break 
  }
  if (error = function_that_could_fail_2() ){
    break 
  }
  ....... 
} while (0) 
cleanup();
.... 

Jeśli funkcje mają ten sam typ parametru, umieść je w tabeli i użyj pętli -

mattnz
źródło
2
Obecne wytyczne MISRA-C: 2004 nie pozwalają na goto w żadnej formie (patrz przepis 14.4). Zauważ, że komitet MISRA zawsze był tym zdezorientowany, nie wiedzą, na której stopie się oprzeć. Po pierwsze, bezwarunkowo zakazali używania goto, kontynuują itd. Ale w projekcie na nadchodzącą MISRA 2011 chcą ponownie na to pozwolić. Na marginesie, proszę zauważyć, że MISRA zakazuje przypisywania w instrukcjach if, z bardzo dobrych powodów, ponieważ jest to znacznie bardziej niebezpieczne niż jakiekolwiek użycie goto.
1
Z analitycznego punktu widzenia dodanie flagi do programu jest równoznaczne z powieleniem całego kodu kodu w zakresie, if(flag)w którym flaga ma zasięg, kazanie każdemu egzemplarzowi pobrać gałąź „if”, a każde odpowiadające mu instrukcje w drugiej kopii - jeszcze". Działania, które ustawiają i usuwają flagę są tak naprawdę „gotami”, które przechodzą między tymi dwiema wersjami kodu. Są chwile, kiedy użycie flag jest czystsze niż jakakolwiek alternatywa, ale dodanie flagi w celu uratowania jednego gotocelu nie jest dobrym kompromisem.
supercat
1

Używam również, gotojeśli alternatywne do/while/continue/breakhakowanie byłoby mniej czytelne.

gotomają tę zaletę, że ich cele mają nazwę i czytają goto something;. Może to być bardziej czytelne niż breaklub continuejeśli faktycznie nie przerywasz lub nie kontynuujesz.

aib
źródło
4
Gdziekolwiek w obrębie tego do ... while(0)lub innego konstruktu, który nie jest rzeczywistą pętlą, ale zharebrained próbą zapobiegania użyciu goto.
aib
1
Ach, dzięki, nie znałem tej konkretnej marki „Dlaczego, do diabła, ktoś miałby to robić ?!” konstruuje jeszcze.
Benjamin Kloster
2
Zazwyczaj hackery do / while / Continue / break stają się nieczytelne tylko wtedy, gdy moduł je zawierający jest zbyt długo frapujący.
John R. Strohm,
2
Nie mogę znaleźć w tym niczego jako uzasadnienia użycia goto. Przerwa i kontynuacja mają oczywistą konsekwencję. iść gdzieś? Gdzie jest etykieta? Break and goto powie ci dokładnie, gdzie jest następny krok i gdzie jest w pobliżu.
Przypon
1
Etykieta powinna oczywiście być widoczna z wnętrza pętli. Zgadzam się z fragmentem komentarza @Johna R. Strohma dotyczącym długości filmu. Twój punkt, przetłumaczony na hakowanie pętli, brzmi: „Wyłam się z tego? To nie jest pętla!”. W każdym razie OP staje się tym, czego obawiał się PO, więc rezygnuję z dyskusji.
aib
-1
for (int y=0; y<height; ++y) {
    for (int x=0; x<width; ++x) {
        if (find(x, y)) goto found;
    }
}
found:
aib
źródło
Jeśli jest tylko jedna pętla, breakdziała dokładnie tak samo goto, choć nie nosi piętna.
9000
6
-1: Po pierwsze, xiy są POZA ZAKRESEM przy znalezieniu :, więc to ci nie pomoże. Po drugie, gdy kod został napisany, fakt, że doszedłeś do wniosku: nie oznacza, że ​​znalazłeś to, czego szukałeś.
John R. Strohm,
To dlatego, że jest to najmniejszy przykład, jaki mogłem wymyślić w przypadku zerwania wielu pętli. Edytuj go, aby uzyskać lepszą etykietę lub gotowy czek.
aib
1
Należy jednak pamiętać, że funkcje C niekoniecznie są pozbawione skutków ubocznych.
aib
1
@ JohnR.Strohm To nie ma większego sensu ... Etykieta „znaleziona” służy do przerwania pętli, a nie do sprawdzenia zmiennych. Gdybym chciał sprawdzić zmienne, mógłbym zrobić coś takiego: for (int y = 0; y <height; ++ y) {for (int x = 0; x <width; ++ x) {if (find ( x, y)) {doSomeThingWith (x, y); znalazłem; }}} znaleziono:
YoYoYonnY
-1

Zawsze będą obozy, które mówią, że jeden sposób jest akceptowalny, a drugi nie. Firmy, w których pracowałem, zmarszczyły brwi lub zdecydowanie odradzają goto używać. Osobiście nie mogę myśleć o żadnym momencie, w którym go użyłem, ale to nie znaczy, że tak złe , tylko inny sposób robienia rzeczy.

W C zazwyczaj wykonuję następujące czynności:

  • Testuj warunki, które mogą uniemożliwić przetwarzanie (złe dane wejściowe itp.) I „powrót”
  • Wykonaj wszystkie kroki wymagające alokacji zasobów (np. Malloc)
  • Wykonaj przetwarzanie, w którym wiele kroków sprawdza sukces
  • Zwolnij wszystkie zasoby, jeśli zostaną pomyślnie przydzielone
  • Zwróć wszelkie wyniki

W celu przetworzenia na przykładzie goto zrobiłbym to:

error = function_that_could_fail_1 (); if (! error) {error = function_that_could_fail_2 (); } if (! error) {error = function_that_could_fail_3 (); }

Nie ma zagnieżdżenia, a wewnątrz klauzul if można wykonać dowolne raportowanie błędów, jeśli krok wygenerował błąd. Więc nie musi być „gorszy” niż metoda wykorzystująca gotos.

Muszę jeszcze natknąć się na przypadek, w którym ktoś ma problemy, których nie da się zrobić inną metodą i jest tak samo czytelny / zrozumiały, i to jest klucz, IMHO.

pcm
źródło