Prawidłowe użycie goto do zarządzania błędami w C?


95

To pytanie jest w rzeczywistości wynikiem interesującej dyskusji, która odbyła się jakiś czas temu na stronieming.reddit.com. Zasadniczo sprowadza się do następującego kodu:

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;
}

Użycie gototutaj wydaje się być najlepszą drogą, skutkując najczystszym i najbardziej wydajnym kodem ze wszystkich możliwości, a przynajmniej tak mi się wydaje. Cytując Steve'a McConnell'a w Code Complete :

Goto jest przydatny w procedurze, która przydziela zasoby, wykonuje operacje na tych zasobach, a następnie zwalnia zasoby. Za pomocą goto możesz wyczyścić jedną sekcję kodu. Goto zmniejsza prawdopodobieństwo, że zapomnisz o zwolnieniu zasobów w każdym miejscu, w którym wykryjesz błąd.

Inne wsparcie dla tego podejścia pochodzi z książki Linux Device Drivers w tej sekcji .

Co myślisz? Czy ten przypadek jest prawidłowy dla gotow C? Czy wolałbyś inne metody, które generują bardziej zawiły i / lub mniej wydajny kod, ale unikają goto?



@Eli: Dlaczego nie usuniesz tagów i nie umieścisz funkcji (cleanup_3 ();) w nawiasie if?

@Akito: co masz na myśli? Czy możesz zamieścić swoją sugestię jako odpowiedź z pełnym przykładem kodu?
Eli Bendersky

@EliBendersky: Zobacz moją odpowiedź.

Jedną z rzeczy, których najbardziej nienawidziłem z Visual Basic (w tym VBS i VB6) był on error goto errorsystem obsługi błędów :)
Manu343726

Odpowiedzi:


64

FWIF, uważam, że idiom obsługi błędów, który podałeś w przykładzie pytania, jest bardziej czytelny i łatwiejszy do zrozumienia niż jakiekolwiek alternatywy podane w dotychczasowych odpowiedziach. Chociaż gotojest to ogólnie zły pomysł, może być przydatny do obsługi błędów, gdy jest wykonywany w prosty i jednolity sposób. W tej sytuacji, mimo że jest to a goto, jest używany w dobrze zdefiniowany i mniej lub bardziej uporządkowany sposób.


1
Czy nie może po prostu usunąć tych tagów i wstawić funkcje bezpośrednio do if? Czy nie będzie to bardziej czytelne?

8
@StartupCrazy, wiem, że to już lata, ale ze względu na aktualność postów na tej stronie zaznaczę, że nie może. Jeśli dostanie błąd przy goto error3 w swoim kodzie, uruchomiłby czyszczenie 1 2 i 3, w twoim sugeestowanym rozwiązaniu przeprowadziłby tylko czyszczenie 3. Mógłby zagnieżdżać rzeczy, ale to byłby tylko strzałka antywzór, czego programiści powinni unikać .
gbtimmon

18

Zasadniczo unikanie goto jest dobrym pomysłem, ale nadużycia, które były powszechne, gdy Dijkstra po raz pierwszy napisał „GOTO uważane za szkodliwe”, nie przychodzą obecnie nawet do głowy większości ludzi jako opcja.

To, co opisujesz, jest uogólnionym rozwiązaniem problemu obsługi błędów - jest w porządku, o ile jest ostrożnie używane.

Twój konkretny przykład można uprościć w następujący sposób (krok 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;
}

Kontynuacja procesu:

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;
}

Uważam, że jest to odpowiednik oryginalnego kodu. Wygląda to szczególnie czysto, ponieważ sam oryginalny kod był bardzo czysty i dobrze zorganizowany. Często fragmenty kodu nie są tak uporządkowane (chociaż zaakceptowałbym argument, że powinny); na przykład często jest więcej stanów do przekazania do procedur inicjalizacyjnych (konfiguracyjnych) niż pokazano, a zatem więcej stanów do przekazania także do procedur czyszczących.


24
Tak, zagnieżdżone rozwiązanie jest jedną z opłacalnych alternatyw. Niestety, wraz z pogłębianiem się poziomu zagnieżdżenia, staje się mniej opłacalny.
Eli Bendersky,

4
@eliben: Zgoda - ale głębsze zagnieżdżenie może wskazywać (prawdopodobnie jest) wskazówką, że musisz wprowadzić więcej funkcji, wykonać więcej czynności przygotowawczych lub w inny sposób zreformować kod. Mógłbym argumentować, że każda z funkcji przygotowujących powinna wykonać swoją konfigurację, wywołać następną w łańcuchu i wykonać własne czyszczenie. Lokalizuje to działanie - możesz nawet zaoszczędzić na trzech funkcjach porządkujących. Zależy to również, po części, od tego, czy którakolwiek z funkcji konfiguracji lub czyszczenia jest używana (użyteczna) w jakiejkolwiek innej sekwencji wywołania.
Jonathan Leffler,

6
Niestety nie działa to z pętlami - jeśli błąd wystąpi w pętli, wtedy goto jest dużo, dużo czystszy niż alternatywa ustawiania i sprawdzania flag i instrukcji „break” (które są po prostu sprytnie zamaskowanymi goto).
Adam Rosenfield

1
@ Smith, Bardziej jak jazda bez kamizelki kuloodpornej.
strager

6
Wiem, że tu nekromantuję, ale uważam, że ta rada jest raczej kiepska - należy unikać anty-wzorca strzały .
KingRadical

16

Dziwię się, że nikt nie zasugerował tej alternatywy, więc nawet jeśli to pytanie trwało jakiś czas, dodam je: jednym z dobrych sposobów rozwiązania tego problemu jest użycie zmiennych do śledzenia bieżącego stanu. Jest to technika, której można użyć bez względu na to, czy gotojest używana do uzyskania kodu porządkującego. Jak każda technika kodowania ma zalety i wady i nie będzie pasować do każdej sytuacji, ale jeśli wybierasz styl, warto go rozważyć - zwłaszcza jeśli chcesz tego uniknąć, gotonie kończąc na głęboko zagnieżdżonych ifs.

Podstawową ideą jest to, że dla każdego działania porządkującego, które może być konieczne, istnieje zmienna, z której wartości możemy określić, czy czyszczenie wymaga wykonania, czy nie.

gotoNajpierw pokażę wersję, ponieważ jest bliżej kodu z oryginalnego pytania.

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;
}

Zaletą tego w porównaniu z niektórymi innymi technikami jest to, że jeśli zmieni się kolejność funkcji inicjalizacyjnych, poprawne czyszczenie będzie nadal miało miejsce - na przykład przy użyciu switchmetody opisanej w innej odpowiedzi, jeśli kolejność inicjalizacji ulegnie zmianie, to switchmusi być bardzo ostrożnie edytowany, aby uniknąć próby wyczyszczenia czegoś, co w rzeczywistości nie zostało zainicjowane.

Niektórzy mogą twierdzić, że ta metoda dodaje całą masę dodatkowych zmiennych - i rzeczywiście w tym przypadku jest to prawda - ale w praktyce często istniejąca zmienna już śledzi lub może być zmuszona do śledzenia wymaganego stanu. Na przykład, jeśli prepare_stuff()faktycznie jest wywołaniem do malloc()lub do open(), wówczas można użyć zmiennej przechowującej zwrócony wskaźnik lub deskryptor pliku - na przykład:

int fd = -1;

....

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

...

cleanup:

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

Teraz, jeśli dodatkowo śledzimy stan błędu za pomocą zmiennej, możemy gotocałkowicie uniknąć i nadal poprawnie wyczyścić, bez wcięć, które stają się coraz głębsze, im więcej potrzebujemy inicjalizacji:

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;
}

Ponownie istnieje potencjalna krytyka tego:

  • Czy te wszystkie „jeśli” nie szkodzą wydajności? Nie - ponieważ w przypadku sukcesu i tak musisz wykonać wszystkie sprawdzenia (w przeciwnym razie nie sprawdzisz wszystkich przypadków błędów); aw przypadku niepowodzenia większość kompilatorów zoptymalizuje sekwencję zakończonych niepowodzeniem if (oksofar)sprawdzeń do pojedynczego skoku do kodu czyszczącego (z pewnością tak robi GCC) - aw każdym razie przypadek błędu jest zwykle mniej krytyczny dla wydajności.
  • Czy to nie dodaje kolejnej zmiennej? W tym przypadku tak, ale często return_valuezmienna może być używana do odgrywania roli, która oksofartutaj gra. Jeśli skonstruujesz swoje funkcje tak, aby zwracały błędy w spójny sposób, możesz nawet uniknąć drugiego ifw każdym przypadku:

    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);
    }
    

    Jedną z zalet takiego kodowania jest to, że spójność oznacza, że ​​każde miejsce, w którym pierwotny programista zapomniał sprawdzić zwracaną wartość, wystaje jak bolący kciuk, co znacznie ułatwia znajdowanie (tej jednej klasy) błędów.

A więc - to (jeszcze) jeszcze jeden styl, który można wykorzystać do rozwiązania tego problemu. Użyty poprawnie pozwala na bardzo czysty, spójny kod - i jak każda technika, w niepowołanych rękach może skończyć się tworzeniem kodu, który jest rozwlekły i zagmatwany :-)


2
Wygląda na to, że spóźniłeś się na przyjęcie, ale z pewnością podoba mi się odpowiedź!

Linus prawdopodobnie odrzuciłby twój kod blogs.oracle.com/oswald/entry/is_goto_the_root_of
Fizz

1
@ user3588161: Jeśli tak, to jego przywilej - ale nie jestem pewien, na podstawie artykułu, do którego utworzyłeś link, że tak jest: zwróć uwagę, że w stylu, który opisuję, (1) warunki warunkowe nie zagnieżdżają się dowolnie głęboko i (2) nie ma żadnych dodatkowych instrukcji „if” w porównaniu z tym, czego i tak potrzebujesz (zakładając, że zamierzasz sprawdzić wszystkie kody powrotu).
psmears,

Tyle to zamiast okropnego goto i jeszcze gorszego rozwiązania typu strzałka-anty-wzór!
Paramagnetic Croissant

8

Problem ze gotosłowem kluczowym jest w większości źle zrozumiany. To nie jest po prostu złe. Musisz tylko zdawać sobie sprawę z dodatkowych ścieżek sterowania, które tworzysz przy każdym goto. Trudno jest uzasadnić swój kod, a tym samym jego ważność.

FWIW, jeśli zajrzysz do samouczków developer.apple.com, przyjmą oni podejście goto do obsługi błędów.

Nie używamy goto. Większe znaczenie ma zwracane wartości. Obsługa wyjątków odbywa się za pomocą setjmp/longjmp- cokolwiek możesz.


8
Chociaż z pewnością użyłem setjmp / longjmp w niektórych przypadkach, gdy było to wymagane, uważam, że jest to nawet „gorsze” niż goto (którego używam również, nieco mniej powściągliwie, gdy jest wywoływany). Używam setjmp / longjmp tylko wtedy, gdy (1) Cel zamknie wszystko w sposób, na który nie będzie miał wpływu jego obecny stan, lub (2) Cel ponownie zainicjuje wszystko kontrolowane w blok setjmp / longjmp chroniony w sposób niezależny od jego obecnego stanu.
supercat

4

Nie ma nic moralnie złego w stwierdzeniu goto, tak jak nie jest coś moralnie nie tak ze wskazówkami (void) *.

Wszystko zależy od tego, jak używasz narzędzia. W przedstawionym przez ciebie (trywialnym) przypadku instrukcja case może osiągnąć tę samą logikę, aczkolwiek z większym narzutem. Prawdziwe pytanie brzmi: „jakie jest moje wymaganie dotyczące prędkości?”

goto jest po prostu szybki, zwłaszcza jeśli uważasz, aby kompilować się do krótkiego skoku. Idealny do zastosowań, w których liczy się szybkość. W przypadku innych aplikacji prawdopodobnie sensowne jest zajęcie się problemem z przypadkiem if / else + dla łatwości konserwacji.

Pamiętaj: goto nie zabija aplikacji, programiści zabijają aplikacje.

UPDATE: Oto przykład przypadku

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
Czy mógłbyś przedstawić alternatywę „przypadku”? Twierdziłbym również, że różni się to znacznie od void *, które jest wymagane w przypadku każdej poważnej abstrakcji struktury danych w C. Nie sądzę, aby ktoś poważnie sprzeciwiał się void * i nie znajdziesz ani jednej dużej bazy kodu bez to.
Eli Bendersky

Re: void *, dokładnie o to mi chodzi, nie ma w tym nic moralnego. Przykład przełącznika / przypadku poniżej. 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 (przygotowania_stuff (bar)) {{return_value = do_the_thing (bar); cleanup_3 (); } switch (failure_value) {przypadek 2: cleanup_2 (); przerwa ; przypadek 1: cleanup_1 (); przerwa ; domyślnie: przerwa; }}
webmarc

5
@webmarc, przepraszam, ale to jest okropne! Po prostu całkowicie zasymulowałeś goto z etykietami - wymyślając własne nieopisowe wartości dla etykiet i implementując goto z przełącznikiem / wielkością liter. Failure_value = 1 jest czystszy niż „goto cleanup_something”?
Eli Bendersky,

4
Czuję, że postawiłeś mnie tutaj ... Twoje pytanie dotyczy opinii, a ja bym wolał. Jednak kiedy udzieliłem odpowiedzi, było to okropne. :-( Jeśli chodzi o Twoją skargę dotyczącą nazw etykiet, są one tak samo opisowe, jak reszta Twojego przykładu: cleanup_1, foo, bar. Dlaczego atakujesz nazwy wytwórni, skoro nie są one nawet związane z pytaniem?
webmarc

1
Nie miałem zamiaru „organizować” i wywoływać jakichkolwiek negatywnych uczuć, przepraszam za to! Po prostu wydaje się, że Twoje nowe podejście jest ukierunkowane wyłącznie na „usuwanie goto”, bez dodawania większej przejrzystości. To tak, jakbyś ponownie zaimplementował to, co robi goto, używając po prostu większej ilości kodu, który jest mniej przejrzysty. To IMHO nie jest dobrą rzeczą - pozbycie się goto tylko ze względu na to.
Eli Bendersky,

2

GOTO jest przydatne. To coś, co potrafi twój procesor i dlatego powinieneś mieć do niego dostęp.

Czasami chcesz dodać coś do swojej funkcji i możesz to łatwo zrobić. Oszczędza czas.


3
Nie potrzebujesz dostępu do wszystkiego, co może zrobić Twój procesor. W większości przypadków goto jest bardziej zagmatwana niż alternatywa.
David Thornley

@DavidThornley: Tak, można zrobić potrzebny jest dostęp do każdej pojedynczej rzeczy procesor może zrobić, w przeciwnym razie tracisz swój procesor. Goto to najlepsza instrukcja programowania.
Ron Maimon,

1

Ogólnie rzecz biorąc, wziąłbym pod uwagę fakt, że fragment kodu można najdokładniej napisać, wykorzystując gotojako symptom, że przepływ programu jest prawdopodobnie bardziej skomplikowany niż jest to ogólnie pożądane. Łączenie innych struktur programu w dziwny sposób, aby uniknąć użycia, gotobyłoby próbą leczenia objawu, a nie choroby. Twój konkretny przykład może nie być zbyt trudny do wdrożenia bez goto:

  zrobić {
    .. skonfiguruj rzecz 1, która będzie wymagała czyszczenia tylko w przypadku wcześniejszego wyjścia
    if (błąd) przerwa;
    zrobić
    {
      .. skonfiguruj rzecz2, która będzie wymagała uporządkowania w przypadku wcześniejszego wyjścia
      if (błąd) przerwa;
      // ***** ZOBACZ TEKST DOTYCZĄCY TEJ LINII
    } podczas (0);
    .. cleanup thing2;
  } podczas (0);
  .. cleanup thing1;

ale jeśli czyszczenie miało się odbyć tylko wtedy, gdy funkcja zawiodła, gotosprawę można by załatwić, umieszczając returntuż przed pierwszą etykietą docelową. Powyższy kod wymagałby dodania returnw linii oznaczonej *****.

W scenariuszu „porządkowanie nawet w normalnym przypadku” uznałbym użycie gotoza jaśniejsze niż konstrukcje do/ while(0), między innymi dlatego, że same etykiety docelowe praktycznie krzyczą „SPÓJRZ NA MNIE” znacznie bardziej niż konstrukcje breaki do/ while(0). W przypadku „czyszczenie tylko w przypadku błędu” returninstrukcja kończy się na tym, że musi znajdować się w najgorszym możliwym miejscu z punktu widzenia czytelności (instrukcje powrotu powinny generalnie znajdować się na początku funkcji lub na tym, jak „wygląda” koniec); posiadanie returnetykiety tuż przed etykietą docelową spełnia tę kwalifikację znacznie łatwiej niż posiadanie etykiety tuż przed końcem „pętli”.

BTW, jeden scenariusz, w którym czasami używam gotodo obsługi błędów, znajduje się w switchinstrukcji, gdy kod dla wielu przypadków ma ten sam kod błędu. Chociaż mój kompilator często byłby wystarczająco inteligentny, aby rozpoznać, że wiele przypadków kończy się tym samym kodem, myślę, że jaśniej jest powiedzieć:

 REPARSE_PACKET:
  przełącznik (pakiet [0])
  {
    sprawa PKT_THIS_OPERATION:
      jeśli (stan problemu)
        goto PACKET_ERROR;
      ... obsłuż THIS_OPERATION
      złamać;
    sprawa PKT_THAT_OPERATION:
      jeśli (stan problemu)
        goto PACKET_ERROR;
      ... obsłuż THAT_OPERATION
      złamać;
    ...
    sprawa PKT_PROCESS_CONDITIONALLY
      if (długość_pakietu <9)
        goto PACKET_ERROR;
      if (warunek_pakietu obejmujący pakiet [4])
      {
        długość_pakietu - = 5;
        memmove (pakiet, pakiet + 5, długość_pakietu);
        goto REPARSE_PACKET;
      }
      jeszcze
      {
        pakiet [0] = PKT_CONDITION_SKIPPED;
        pakiet [4] = długość_pakietu;
        długość_pakietu = 5;
        packet_status = READY_TO_SEND;
      }
      złamać;
    ...
    domyślna:
    {
     PACKET_ERROR:
      packet_error_count ++;
      packet_length = 4;
      pakiet [0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      złamać;
    }
  }   

Chociaż można by zamienić gotoinstrukcje na {handle_error(); break;}i chociaż można by użyć pętli do/ while(0)wraz z continueprzetwarzaniem opakowanego pakietu warunkowego wykonania, tak naprawdę nie sądzę, aby było to bardziej przejrzyste niż użycie goto. Co więcej, chociaż może być możliwe skopiowanie kodu z PACKET_ERRORdowolnego miejsca, w którym goto PACKET_ERRORjest używany, i chociaż kompilator może napisać zduplikowany kod raz i zastąpić większość wystąpień skokiem do tej udostępnionej kopii, użycie narzędzia gotoułatwia zauważenie miejsc które ustawiają pakiet trochę inaczej (np. jeśli instrukcja „wykonaj warunkowo” zdecyduje się nie wykonywać).


1

Osobiście jestem zwolennikiem „Siła dziesięciu - 10 zasad pisania krytycznego kodu bezpieczeństwa” .

Dołączę mały fragment tego tekstu, który ilustruje, jak sądzę, dobry pomysł na temat goto.


Reguła: Ogranicz cały kod do bardzo prostych konstrukcji przepływu sterowania - nie używaj instrukcji goto, konstrukcji setjmp lub longjmp oraz rekursji bezpośredniej lub pośredniej.

Uzasadnienie: Prostszy przepływ kontroli przekłada się na lepsze możliwości weryfikacji i często skutkuje lepszą przejrzystością kodu. Wygnanie rekurencji jest tutaj chyba największą niespodzianką. Jednak bez rekurencji mamy gwarancję, że mamy graf wywołania acyklicznego funkcji, który może być wykorzystany przez analizatory kodu i może bezpośrednio pomóc w udowodnieniu, że wszystkie wykonania, które powinny być ograniczone, są w rzeczywistości ograniczone. (Należy zauważyć, że ta reguła nie wymaga, aby wszystkie funkcje miały jeden punkt powrotu - chociaż często upraszcza to również przepływ sterowania. Jest jednak wystarczająco dużo przypadków, w których wczesne zwrócenie błędu jest prostszym rozwiązaniem).


Wykluczenie używania goto wydaje się złe, ale:

Jeśli na pierwszy rzut oka zasady wydają się drakońskie, pamiętaj, że mają na celu umożliwienie sprawdzenia kodu, w którym bardzo dosłownie twoje życie może zależeć od jego poprawności: kod używany do kontrolowania samolotu, którym lecisz, elektrowni jądrowej kilka mil od miejsca, w którym mieszkasz, lub statek kosmiczny, który przenosi astronautów na orbitę. Zasady działają jak pasy bezpieczeństwa w Twoim aucie: początkowo mogą być trochę niewygodne, ale po pewnym czasie ich użycie staje się drugą naturą i niewyobrażalne staje się ich niewyobrażalne.


22
Problem polega na tym, że zwykłym sposobem na całkowite wygnanie gotojest użycie jakiegoś zestawu „sprytnych” wartości logicznych w głęboko zagnieżdżonych ifach lub pętlach. To naprawdę nie pomaga. Może Twoje narzędzia będą go grok lepiej, ale ty nie jesteś i będzie ważniejsze.
Donal Fellows

1

Zgadzam się, że porządkowanie goto w odwrotnej kolejności podanej w pytaniu jest najczystszym sposobem na uporządkowanie większości funkcji. Ale chciałem też zwrócić uwagę, że czasami i tak chcesz wyczyścić swoją funkcję. W takich przypadkach używam następującego wariantu if (0) {label:} idiom, aby przejść do właściwego punktu procesu czyszczenia:

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

Wydaje mi się, że cleanup_3powinien to zrobić, a potem zadzwoń cleanup_2. Podobnie, cleanup_2należy wykonać czyszczenie, a następnie wywołać cleanup_1. Wygląda na to, że za każdym razem, gdy to zrobisz cleanup_[n], cleanup_[n-1]jest to wymagane, dlatego powinna być odpowiedzialna za metodę (aby na przykład cleanup_3nigdy nie można było wywołać bez wywołania cleanup_2i prawdopodobnie powodując wyciek).

Biorąc pod uwagę takie podejście, zamiast gotos, po prostu wywołałbyś procedurę czyszczenia, a następnie wrócił.

To gotopodejście nie jest złe ani złe , warto jednak zauważyć, że niekoniecznie jest to „najczystsze” podejście (IMHO).

Jeśli szukasz optymalnej wydajności to przypuszczam, że gotorozwiązanie jest najlepsze. Oczekuję jednak, że będzie to istotne tylko w kilku wybranych aplikacjach krytycznych dla wydajności (np. Sterowniki urządzeń, urządzenia wbudowane itp.). W przeciwnym razie jest to mikro-optymalizacja, która ma niższy priorytet niż przejrzystość kodu.


4
To się nie uda - porządki są specyficzne dla zasobów, które są przydzielane w tej kolejności tylko w tej procedurze. W innych miejscach nie są spokrewnieni, więc dzwonienie do jednego od drugiego nie ma sensu.
Eli Bendersky,

0

Myślę, że to pytanie jest błędne w stosunku do podanego kodu.

Rozważać:

  1. do_something (), init_stuff () i ready_stuff () wydają się wiedzieć, czy się nie udało, ponieważ zwracają w takim przypadku wartość false lub nil.
  2. Wydaje się, że odpowiedzialność za konfigurowanie stanu spoczywa na tych funkcjach, ponieważ żaden stan nie jest konfigurowany bezpośrednio w foo ().

Dlatego: do_something (), init_stuff () i Preparat_stuff () powinny wykonywać swoje własne porządki . Posiadanie oddzielnej funkcji cleanup_1 (), która czyści po do_something (), łamie filozofię hermetyzacji. To zły projekt.

Jeśli zrobili własne czyszczenie, foo () staje się całkiem proste.

Z drugiej strony. Jeśli foo () faktycznie stworzyło swój własny stan, który należało zburzyć, to goto byłoby odpowiednie.


0

Oto, co wolałem:

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

Stara dyskusja jednak… co powiesz na użycie „strzałki anty-wzorca” i późniejsze hermetyzowanie każdego zagnieżdżonego poziomu w statycznej funkcji wbudowanej? Kod wygląda na czysty, jest optymalny (gdy optymalizacje są włączone) i nie jest używane goto. Krótko mówiąc, dziel i zwyciężaj. Poniżej przykład:

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;
}

Jeśli chodzi o przestrzeń, tworzymy trzykrotną zmienną na stosie, co nie jest dobre, ale to znika podczas kompilacji z -O2, usuwając zmienną ze stosu i używając rejestru w tym prostym przykładzie. To, co dostałem z powyższego bloku, gcc -S -O2 test.cto poniższe:

    .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

Wolę używać techniki opisanej w poniższym przykładzie ...

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);

}

źródło: http://blog.staila.com/?p=114


2
kod flagowy i anty-wzorzec strzałki (oba pokazane w twoim przykładzie) to rzeczy, które niepotrzebnie komplikują kod. Nie ma żadnego usprawiedliwienia poza „goto jest zło”, aby ich używać.
KingRadical

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.