Jakie są lepsze sposoby na uniknięcie do-while (0); włamać się w C ++?

233

Gdy przepływ kodu wygląda następująco:

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}

Ogólnie widziałem, jak to działa, aby uniknąć powyższego nieporządnego przepływu kodu:

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Jakie są lepsze sposoby uniknięcia tego obejścia / włamania, aby stał się kodem wyższego poziomu (branżowego)?

Wszelkie sugestie, które są gotowe, są mile widziane!

Sankalp
źródło
38
RAII i zgłaszaj wyjątki.
ta.speot.is
135
Wydaje mi się, że to dobry moment na wykorzystanie goto- ale jestem pewien, że ktoś oceni mnie za sugestię, więc nie piszę odpowiedzi na ten temat. DŁUGO do ... while(0);wydaje się niewłaściwą rzeczą.
Mats Petersson,
42
@dasblinkenlight: Tak, rzeczywiście. Jeśli masz zamiar użyć goto, bądź szczery i rób to na zewnątrz, nie ukrywaj go za pomocą breakido ... while
Mats Petersson
44
@MatsPetersson: Daj odpowiedź, dam +1 za gotowysiłek rehabilitacyjny. :)
wilx
27
@ ta.speot.is: „RAII i zgłaszaj wyjątki”. W ten sposób będziesz emulować kontrolę przepływu z wyjątkami. To jest trochę jak użycie drogiego, najnowocześniejszego sprzętu jako młotka lub przycisku do papieru. Możesz to zrobić, ale na pewno wygląda to na bardzo zły gust.
SigTerm,

Odpowiedzi:

309

Uznaje się za dopuszczalną praktykę izolowanie tych decyzji w funkcji i stosowanie returns zamiast breaks. Chociaż wszystkie te kontrole odpowiadają temu samemu poziomowi abstrakcji co funkcja, jest to dość logiczne podejście.

Na przykład:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}
Michaił
źródło
22
@MatsPetersson: „Izoluj w funkcji” oznacza refaktoryzację do nowej funkcji, która wykonuje tylko testy.
MSalters
35
+1. To także dobra odpowiedź. W C ++ 11 funkcja izolowana może być lambda, ponieważ może również przechwytywać zmienne lokalne, więc ułatwia to!
Nawaz
4
@deworde: W zależności od sytuacji to rozwiązanie może stać się znacznie dłuższe i mniej czytelne niż goto. Ponieważ C ++ niestety nie pozwala na zdefiniowanie funkcji lokalnych, będziesz musiał przenieść tę funkcję (tę, z której powrócisz) gdzie indziej, co zmniejsza czytelność. Może się to skończyć z dziesiątkami parametrów, dlatego, ponieważ dziesiątki parametrów to zła rzecz, zdecydujesz się zawinąć je w strukturę, która utworzy nowy typ danych. Zbyt dużo pisania na prostą sytuację.
SigTerm,
24
@Damon, jeśli cokolwiek, returnjest czystszy, ponieważ każdy czytelnik natychmiast wie, że działa poprawnie i co robi. Ze gototrzeba się rozejrzeć, aby zobaczyć, co to jest za, i mieć pewność, że nie popełniono błędy. Przebranie jest zaletą.
R. Martinho Fernandes,
11
@SigTerm: Czasami kod powinien zostać przeniesiony do osobnej funkcji tylko po to, aby utrzymać rozmiar każdej funkcji na poziomie łatwym do uzasadnienia. Jeśli nie chcesz płacić za wywołanie funkcji, zaznacz je forceinline.
Ben Voigt
257

Są chwile, kiedy używanie gotojest w rzeczywistości PRAWĄ odpowiedzią - przynajmniej dla tych, którzy nie wychowali się w przekonaniu religijnym, że „ gotonigdy nie może być odpowiedzią, bez względu na pytanie” - i to jest jeden z tych przypadków.

Ten kod używa hackowania do { ... } while(0);wyłącznie w celu przebrania się gotoza break. Jeśli zamierzasz użyć goto, bądź otwarty na ten temat. Nie ma sensu sprawiać, że kod jest trudniejszy do odczytania.

Szczególna sytuacja występuje wtedy, gdy masz dużo kodu o dość skomplikowanych warunkach:

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}

Może sprawić, że JEST CZYSTSZY, że kod jest poprawny, ponieważ nie ma tak złożonej logiki.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}

Edycja: Aby wyjaśnić, w żadnym wypadku nie proponuję zastosowania gotojako ogólnego rozwiązania. Ale są przypadki, w których gotojest lepsze rozwiązanie niż inne rozwiązania.

Wyobraźmy sobie na przykład, że zbieramy niektóre dane, a różne warunki, które są testowane, to coś w rodzaju „to koniec gromadzonych danych” - który zależy od pewnego rodzaju znaczników „kontynuuj / kończ”, które różnią się w zależności od tego, gdzie jesteś w strumieniu danych.

Teraz, kiedy skończymy, musimy zapisać dane w pliku.

I tak, często istnieją inne rozwiązania, które mogą zapewnić rozsądne rozwiązanie, ale nie zawsze.

Mats Petersson
źródło
76
Nie zgadzać się. gotomoże mieć miejsce, ale goto cleanupnie ma. Czyszczenie odbywa się za pomocą RAII.
MSalters
14
@MSalters: Zakłada się, że czyszczenie wiąże się z czymś, co można rozwiązać za pomocą RAII. Może powinienem zamiast tego powiedzieć „błąd wydania” lub coś podobnego.
Mats Petersson
25
Tak więc, ciągle wypowiadaj się na ten temat, prawdopodobnie z przekonań religijnych, że gotonigdy nie jest właściwą odpowiedzią. Byłbym wdzięczny, gdyby pojawił się komentarz ...
Mats Petersson
19
ludzie nienawidzą, gotoponieważ musisz pomyśleć, aby go użyć / zrozumieć program, który go używa ... Z drugiej strony mikroprocesory są zbudowane jumpsi conditional jumps... więc problem dotyczy niektórych osób, a nie logiki lub czegoś innego.
woliveirajr
17
+1 za właściwe użycie goto. RAII nie jest właściwym rozwiązaniem, jeśli spodziewane są błędy (nie wyjątkowe), ponieważ byłoby to nadużyciem wyjątków.
Joe
82

Możesz użyć prostego wzorca kontynuacji ze boolzmienną:

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

Ten łańcuch wykonania zatrzyma się, gdy tylko checkNzwróci a false. Żadne dalsze check...()połączenia nie będą wykonywane z powodu zwarcia &&operatora. Ponadto optymalizacja kodu wynikowego są wystarczająco inteligentny, aby uznać, że ustawienie goOnna falseto ulica jednokierunkowa i wstawić brakujące goto enddla Ciebie. W rezultacie wydajność powyższego kodu byłaby identyczna z wydajnością a do/ while(0), tylko bez bolesnego pogorszenia jego czytelności.

dasblinkenlight
źródło
30
Zadania w ifwarunkach wewnętrznych wyglądają bardzo podejrzanie.
Michaił
90
@Mikhail Tylko dla niewprawnego oka.
dasblinkenlight
20
Użyłem tej techniki wcześniej i zawsze mnie to wkurzyło, dlatego pomyślałem, że kompilator będzie musiał wygenerować kod, aby sprawdzić każdy, ifbez względu na to, co się stanie, goOnnawet jeśli wczesny nie powiedzie się (w przeciwieństwie do wyskakiwania / wyłamywania się) ... ale właśnie przeprowadziłem test, a VS2012 przynajmniej był wystarczająco inteligentny, aby i tak wszystko zewrzeć po pierwszym fałszywce. Będę korzystał z tego częściej. Uwaga: jeśli użyjesz, goOn &= checkN()to checkN()zawsze będzie działał, nawet jeśli goOnbył falsena początku if(tzn. nie rób tego).
zaznacz
11
@Nawaz: Jeśli masz niedoświadczony umysł, który dokonuje arbitralnych zmian w całej bazie kodu, masz o wiele większy problem niż zwykłe przypisania if.
Idelic
6
@sisharp Elegancja jest tak w oku patrzącego! Nie rozumiem, jak na świecie niewłaściwe użycie zapętlonej konstrukcji może być postrzegane jako „eleganckie”, ale być może to tylko ja.
dasblinkenlight
38
  1. Spróbuj wyodrębnić kod do oddzielnej funkcji (lub więcej niż jednej). Następnie powróć z funkcji, jeśli sprawdzenie się nie powiedzie.

  2. Jeśli jest to zbyt ściśle powiązane z otaczającym kodem, aby to zrobić, a nie możesz znaleźć sposobu na zmniejszenie sprzężenia, spójrz na kod po tym bloku. Przypuszczalnie czyści niektóre zasoby używane przez funkcję. Spróbuj zarządzać tymi zasobami za pomocą obiektu RAII ; następnie zastąpić każdy podejrzanie breakz return(lub throw, jeżeli jest to bardziej właściwe) i niech destructor oczyścić obiektu dla Ciebie.

  3. Jeśli przepływ programu jest (koniecznie) tak zawijasowy, że naprawdę potrzebujesz go goto, użyj go zamiast dawać dziwne przebranie.

  4. Jeśli masz reguły kodowania, które na ślepo zabraniają gotoi naprawdę nie możesz uprościć przepływu programu, prawdopodobnie będziesz musiał ukryć to za pomocą dohacka.

Mike Seymour
źródło
4
Pokornie twierdzę, że RAII, chociaż jest użyteczne, nie jest srebrną kulą. Kiedy masz zamiar napisać klasę konwersji-goto-na-RAII, która nie ma innego zastosowania, zdecydowanie myślę, że lepiej byłoby, gdybyś użył idiomu „goto-end-of-the world”, o którym już wspominali.
idoby
1
@busy_wait: Rzeczywiście, RAII nie może rozwiązać wszystkiego; dlatego moja odpowiedź nie kończy się na drugim punkcie, ale sugeruje, gotoczy to naprawdę lepsza opcja.
Mike Seymour
3
Zgadzam się, ale myślę, że pisanie klas konwersji goto-RAII jest złym pomysłem i myślę, że należy to wyraźnie zaznaczyć.
idoby
@idoby co powiesz na napisanie jednej klasy szablonów RAII i utworzenie jej za pomocą dowolnego czyszczenia, którego potrzebujesz w lambda
Caleth
@Caleth brzmi dla mnie jakbyś odkrywał na nowo lekarzy / lekarzy?
idoby
37

TLDR : RAII , kod transakcyjny (ustaw wyniki lub zwracaj rzeczy tylko wtedy, gdy jest już obliczony) i wyjątki.

Długa odpowiedź:

W C najlepszą praktyką dla tego rodzaju kodu jest dodanie EXIT / CLEANUP / other do kodu etykiety , w której następuje czyszczenie lokalnych zasobów i zwracany jest kod błędu (jeśli taki istnieje). Jest to najlepsza praktyka, ponieważ naturalnie dzieli kod na inicjalizację, obliczenia, zatwierdzanie i zwracanie:

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}

W C, w większości codebases The if(error_ok != ...a gotokod jest zwykle ukryte za niektórych makr (convenience RET(computation_result), ENSURE_SUCCESS(computation_result, return_code)itp).

C ++ oferuje dodatkowe narzędzia w stosunku do C :

  • Funkcję bloku czyszczenia można zaimplementować jako RAII, co oznacza, że ​​nie potrzebujesz już całego cleanupbloku i umożliwiasz kodowi klienta dodawanie instrukcji wczesnego powrotu.

  • Rzucasz, gdy nie możesz kontynuować, przekształcając wszystkie if(error_ok != ... w bezpośrednie połączenia.

Równoważny kod C ++:

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}

Jest to najlepsza praktyka, ponieważ:

  • Jest jawny (to znaczy, podczas gdy obsługa błędów nie jest jawna, główny przepływ algorytmu jest)

  • Pisanie kodu klienta jest proste

  • To jest minimalne

  • To jest proste

  • Nie ma powtarzalnych konstrukcji kodu

  • Nie używa makr

  • To nie jest dziwne do { ... } while(0) konstrukcji

  • Można go ponownie wykorzystać przy minimalnym wysiłku (to znaczy, jeśli chcę skopiować wywołanie do computation2();innej funkcji, nie muszę się upewnić, że dodałem do { ... } while(0)nowy kod, ani #definemakra opakowania goto i etykiety czyszczenia, ani coś jeszcze).

utnapistim
źródło
+1. Właśnie tego próbuję użyć, używając RAII lub czegoś takiego. shared_ptrz niestandardowym urządzeniem do usuwania może zrobić wiele rzeczy. Jeszcze łatwiejsze dzięki lambdom w C ++ 11.
Macke,
To prawda, ale jeśli w końcu użyjesz parametru shared_ptr do rzeczy, które nie są wskaźnikami, rozważ przynajmniej jego wpisanie : namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };W tym przypadku (z make_handleustawieniem poprawnego typu usuwacza podczas budowy) nazwa typu nie sugeruje, że jest to wskaźnik .
utnapistim
21

Dodam odpowiedź ze względu na kompletność. Wiele innych odpowiedzi wskazywało, że duży blok warunków można podzielić na osobną funkcję. Ale jak już wielokrotnie podkreślano, takie podejście oddziela kod warunkowy od pierwotnego kontekstu. Jest to jeden z powodów, dla których lambda zostały dodane do języka w C ++ 11. Stosowanie lambdas było sugerowane przez innych, ale nie podano wyraźnej próbki. Włożyłem jedną do tej odpowiedzi. Uderza mnie to, że do { } while(0)pod wieloma względami jest bardzo podobne do tego podejścia - i może to oznacza, że ​​nadal jest gotow przebraniu ...

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations
Peter R.
źródło
7
Dla mnie ten hack wygląda gorzej niż do ... podczas hackowania.
Michael,
18

Na pewno nie odpowiedź, ale odpowiedź (w trosce o kompletność)

Zamiast :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Możesz napisać:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

To wciąż goto w przebraniu, ale przynajmniej nie jest to już pętla. Co oznacza, że ​​nie będziesz musiał bardzo dokładnie sprawdzać, czy nie ma kontynuacji gdzieś w bloku .

Konstrukcja jest również na tyle prosta, że ​​można mieć nadzieję, że kompilator ją zoptymalizuje.

Jak sugeruje @jamesdlin, możesz nawet ukryć to za makrem jak

#define BLOC switch(0) case 0:

I używaj go jak

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

Jest to możliwe, ponieważ składnia języka C oczekuje po przełączniku instrukcji, a nie bloku w nawiasach, a przed tą instrukcją można umieścić etykietę sprawy. Do tej pory nie widziałem sensu na to pozwalać, ale w tym konkretnym przypadku dobrze jest ukryć przełącznik za ładnym makro.

Kriss
źródło
2
O, sprytne. Możesz nawet ukryć go za makrem like define BLOCK switch (0) case 0:i używać go jak BLOCK { ... break; }.
jamesdlin
@jamesdlin: nigdy wcześniej nie przyszło mi do głowy, że warto położyć skrzynkę przed otwierającym wspornikiem przełącznika. Ale rzeczywiście jest to dozwolone przez C iw tym przypadku dobrze jest napisać ładne makro.
Kriss,
15

Poleciłbym podejście podobne do odpowiedzi Matsa minus niepotrzebne goto. W funkcji umieść tylko logikę warunkową. Każdy kod, który zawsze się uruchamia, powinien przejść przed wywoływaniem funkcji lub po jej wywołaniu:

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}
Dan Bechard
źródło
3
Jeśli musisz zdobyć inny zasób w środku func, musisz oddzielić inną funkcję (zgodnie ze swoim wzorcem). Jeśli wszystkie te izolowane funkcje potrzebują tych samych danych, po prostu raz po raz skopiujesz te same argumenty stosu lub zdecydujesz się przydzielić argumenty na stercie i przekazać wskaźnik, nie wykorzystując najbardziej podstawowej funkcji języka ( argumenty funkcji). Krótko mówiąc, nie sądzę, aby to rozwiązanie można było skalować w najgorszym przypadku, w którym każdy warunek jest sprawdzany po uzyskaniu nowych zasobów, które należy oczyścić. Uwaga Nawaz skomentuje jednak jagnięta.
tne
2
Nie pamiętam, żeby OP mówił o pozyskiwaniu zasobów w środku swojego bloku kodu, ale akceptuję to jako wykonalny wymóg. W takim razie, co jest złego w zadeklarowaniu zasobu na stosie w dowolnym miejscu func()i umożliwieniu jego niszczycielowi obsługi uwolnienia zasobów? Jeśli cokolwiek poza func()dostępem wymaga dostępu do tego samego zasobu, powinno zostać zadeklarowane na stercie przed wywołaniem func()przez odpowiedniego menedżera zasobów.
Dan Bechard
12

Sam przepływ kodu jest już zapachem kodu, który zbyt wiele dzieje się w funkcji. Jeśli nie ma bezpośredniego rozwiązania tego problemu (funkcja jest funkcją sprawdzania ogólnego), lepiej użyć RAII, aby powrócić zamiast przeskakiwać do końcowej sekcji funkcji.

stefaanv
źródło
11

Jeśli nie musisz wprowadzać zmiennych lokalnych podczas wykonywania, często możesz spłaszczyć to:

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()
the_mandrill
źródło
2
Ale wtedy każdy warunek musiałby obejmować poprzedni, który jest nie tylko brzydki, ale potencjalnie zły dla wydajności. Lepiej przeskoczyć te kontrole i oczyścić natychmiast, gdy tylko wiemy, że „nie jest OK”.
tne
To prawda, jednak takie podejście ma zalety, jeśli są rzeczy, które należy zrobić na końcu, więc nie chcesz wracać wcześniej (patrz edycja). Jeśli połączysz to z podejściem Denise do używania booli w danych warunkach, osiągi wydajności będą znikome, chyba że będzie to bardzo ciasna pętla.
the_mandrill
10

Podobne do odpowiedzi dasblinkenlight, ale unika przypisania wewnątrz, ifktóre może zostać „naprawione” przez recenzenta kodu:

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

Używam tego wzorca, gdy wyniki kroku muszą zostać sprawdzone przed następnym krokiem, co różni się od sytuacji, w której wszystkie kontrole można wykonać z góry za pomocą if( check1() && check2()...wzorca dużego typu.

Denise Skidmore
źródło
Zauważ, że check1 () może być naprawdę PerformStep1 (), który zwraca kod wyniku kroku. Pozwoliłoby to ograniczyć złożoność funkcji przepływu procesów.
Denise Skidmore,
10

Użyj wyjątków. Twój kod będzie wyglądał na znacznie czystszy (a wyjątki zostały utworzone właśnie w celu obsługi błędów w przepływie wykonania programu). Aby wyczyścić zasoby (deskryptory plików, połączenia z bazami danych itp.), Przeczytaj artykuł Dlaczego C ++ nie zapewnia konstrukcji „nareszcie”? .

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}
Cartucho
źródło
10
Naprawdę? Po pierwsze, szczerze mówiąc, nie widzę żadnej poprawy czytelności. NIE UŻYWAJ WYJĄTKÓW DO KONTROLI PRZEPŁYWU PROGRAMU. To NIE JEST ich właściwy cel. Ponadto wyjątki niosą ze sobą znaczne kary za wyniki. Właściwym przypadkiem użycia wyjątku jest sytuacja, w której występuje jakiś warunek, ŻE NIE MOŻESZ ZROBIĆ WSZYSTKO, na przykład próba otwarcia pliku, który jest już zablokowany wyłącznie przez inny proces, połączenie sieciowe nie działa lub połączenie z bazą danych nie działa lub osoba dzwoniąca przekazuje nieprawidłowy parametr do procedury. Coś w tym rodzaju. Ale NIE używaj wyjątków do kontrolowania przebiegu programu.
Craig,
3
Chodzi mi o to, żeby nie być głupkiem na ten temat, ale przejdź do artykułu Stroustrup, do którego się odwoływałeś i poszukaj „Czego nie powinienem używać wyjątków?” Sekcja. Mówi między innymi: „W szczególności rzut nie jest po prostu alternatywnym sposobem zwracania wartości z funkcji (podobnie jak return). Powoduje to powolność i dezorientuje większość programistów C ++ przyzwyczajonych do zauważania wyjątków używanych tylko w przypadku błędu obsługa. Podobnie rzut nie jest dobrym sposobem na wyjście z pętli. ”
Craig,
3
@Craig Wszystko, co wskazałeś, jest słuszne, ale zakładasz, że program próbny może być kontynuowany po check()awarii warunku, a to z pewnością TWOJE założenie, że w próbce nie ma kontekstu. Zakładając, że program nie może być kontynuowany, należy stosować wyjątki.
Cartucho,
2
Cóż, to prawda, jeśli tak naprawdę jest kontekst. Ale zabawna rzecz o założeniach ... ;-)
Craig
10

Dla mnie do{...}while(0)jest w porządku. Jeśli nie chcesz do{...}while(0), możesz zdefiniować dla nich alternatywne słowa kluczowe.

Przykład:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST do{
#define END_TEST }while(0);

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;

   //processing code here

END_TEST

Myślę, że kompilator usunie niepotrzebne while(0)warunki wdo{...}while(0) wersji binarnej i przekształci przerwy w bezwarunkowy skok. Dla pewności możesz sprawdzić jego wersję językową asemblera.

Użycie gotorównież zapewnia czystszy kod i jest to proste dzięki logice warunku, a następnie skoku. Możesz wykonać następujące czynności:

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;

   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.

Uwaga: etykieta jest umieszczana po zamknięciu }. Jest to uniknięcie jednego możliwego problemu gotopolegającego na przypadkowym umieszczeniu kodu pośredniego, ponieważ etykieta nie była widoczna. Jest teraz jak do{...}while(0)bez kodu warunkowego.

Aby uczynić ten kod czystszym i bardziej zrozumiałym, możesz to zrobić:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)

Dzięki temu możesz wykonywać zagnieżdżone bloki i określać, gdzie chcesz wyjść / wyskoczyć.

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);

      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)

Kod spaghetti to nie wina goto, to wina programisty. Nadal możesz produkować kod spaghetti bez użycia goto.

acegs
źródło
10
Wybrałbym gotoponad rozszerzenie składni języka za pomocą preprocesora milion razy.
Christian
2
„Aby uczynić ten kod czystszym i bardziej zrozumiałym, możesz [użyć LOADS_OF_WEIRD_MACROS]” : nie oblicza.
underscore_d
8

Jest to dobrze znany i dobrze rozwiązany problem z funkcjonalnego punktu widzenia programowania - być może monada.

W odpowiedzi na komentarz, który otrzymałem poniżej, zredagowałem moje wprowadzenie tutaj: Pełne informacje na temat implementacji monad C ++ w różnych miejscach pozwolą ci osiągnąć to, co sugeruje Rotsor. Grokowanie monad zajmuje trochę czasu, dlatego zamiast tego zasugeruję tutaj szybki, podobny do monady mechanizm, dla którego musisz wiedzieć tylko o boost :: opcjonalnym.

Skonfiguruj kroki obliczeniowe w następujący sposób:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

Każdy krok obliczeniowy może oczywiście zrobić coś takiego jak return, boost::nonejeśli podany opcjonalny jest pusty. Na przykład:

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}

Następnie połącz je razem:

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}

Zaletą tego jest to, że możesz pisać jasno zdefiniowane testy jednostkowe dla każdego kroku obliczeniowego. Również wywołanie brzmi jak zwykły angielski (jak zwykle w przypadku funkcjonalnego stylu).

Jeśli nie zależy ci na niezmienności i wygodniej jest zwracać ten sam obiekt za każdym razem, gdy możesz wymyślić jakąś odmianę za pomocą shared_ptr lub podobnego.

Benedykt
źródło
3
Ten kod ma niepożądaną właściwość polegającą na zmuszaniu każdej z poszczególnych funkcji do radzenia sobie z awarią poprzedniej funkcji, a tym samym niepoprawnego wykorzystania idiomu Monady (gdzie efekty monadyczne, w tym przypadku awarię, należy traktować niejawnie). Aby to zrobić, musisz optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);zamiast tego użyć funkcji kompozycji monadycznej („bind”) zamiast aplikacji funkcji.
Rotsor
Dzięki. Masz rację - w ten sposób możesz to zrobić poprawnie. W mojej odpowiedzi nie chciałem pisać zbyt wiele, aby to wyjaśnić (stąd termin „biedak”, który miał sugerować, że nie zamierzam tu chodzić po całym świecie).
Benedykt
7

Co powiesz na przeniesienie instrukcji if do dodatkowej funkcji dającej wynik liczbowy lub wyliczeniowy?

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}
karx11erx
źródło
Jest to miłe, gdy jest to możliwe, ale o wiele mniej ogólne niż zadane tutaj pytanie. Każdy warunek może zależeć od kodu wykonanego po ostatnim teście usuwania błędów.
kriss
Problemem jest tutaj oddzielna przyczyna i konsekwencja. Oznacza to, że masz oddzielny kod, który odnosi się do tego samego numeru warunku i może to być źródłem dalszych błędów.
Ryga
@kriss: Cóż, funkcja ConditionCode () może być dostosowana, aby się tym zająć. Kluczowym punktem tej funkcji jest to, że można użyć return <result> dla czystego wyjścia, jak tylko zostanie obliczony końcowy warunek; I to zapewnia tutaj strukturalną przejrzystość.
karx11erx
@Riga: Imo, to są całkowicie akademickie zastrzeżenia. Uważam, że C ++ staje się bardziej złożony, tajemniczy i nieczytelny z każdą nową wersją. Nie widzę problemu z małą funkcją pomocnika oceniającą złożone warunki w dobrze ustrukturyzowany sposób, aby funkcja celu znajdująca się poniżej była bardziej czytelna.
karx11erx
1
@ karx11erx moje zastrzeżenia są praktyczne i oparte na moim doświadczeniu. Ten wzorzec jest niepowiązany z C ++ 11 lub jakimkolwiek językiem. Jeśli masz problemy z konstrukcjami językowymi, które pozwalają pisać dobrą architekturę, która nie stanowi problemu językowego.
Ryga
5

Być może coś takiego

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

lub użyj wyjątków

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

Korzystając z wyjątków, możesz również przekazywać dane.

liftarn
źródło
10
Nie rób mądrych rzeczy, takich jak twoja definicja, ponieważ zwykle utrudniają czytanie kodu innym programistom. Widziałem, jak ktoś definiuje Case jako break; case w pliku nagłówkowym, i użyłem go w przełączniku w pliku cpp, co sprawia, że ​​inni zastanawiają się godzinami, dlaczego przełącznik przerywa się między instrukcjami Case. Grrr ...
Michael
5
A kiedy nazywasz makra, powinieneś sprawić, by wyglądały jak makra (tzn. Wielkimi literami). W przeciwnym razie ktoś, kto zdoła nazwać zmienną / funkcję / typ / itp. o imieniuever będzie bardzo niezadowolony ...
jamesdlin
5

Nie przepadam za używaniem breakani returnw takim przypadku. Biorąc pod uwagę, że zwykle, gdy mamy do czynienia z taką sytuacją, jest to zwykle metoda stosunkowo długa.

Jeśli mamy wiele punktów wyjścia, może to powodować trudności, gdy chcemy wiedzieć, co spowoduje wykonanie pewnej logiki: normalnie po prostu przechodzimy w górę bloków zawierających tę logikę, a kryteria tych blokujących bloków mówią nam, że sytuacja:

Na przykład,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

Patrząc na otaczające bloki, łatwo jest stwierdzić, że myLogic()dzieje się to tylko wtedy, gdy conditionA and conditionB and conditionCjest to prawdą.

Staje się znacznie mniej widoczny, gdy pojawiają się wczesne zwroty:

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

Nie możemy już nawigować w górę myLogic(), patrząc na otaczający blok, aby ustalić warunek.

Istnieją różne obejścia, których użyłem. Oto jeden z nich:

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}

(Oczywiście mile widziane jest użycie tej samej zmiennej do zastąpienia wszystkich isA isB isC .)

Takie podejście da przynajmniej czytelnikowi kodu, który myLogic()jest wykonywany, kiedy isB && conditionC. Czytelnik otrzymuje wskazówkę, że musi dalej sprawdzać, co spowoduje, że B jest prawdą.

Adrian Shum
źródło
3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

Co ty na to?

sniperbat
źródło
if jest przestarzały, po prostu return condition;, w przeciwnym razie uważam, że jest to łatwe do utrzymania.
SpaceTrucker
2

Kolejny wzór przydatny, jeśli potrzebujesz różnych kroków czyszczenia w zależności od tego, gdzie jest awaria:

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }
Denise Skidmore
źródło
2

Nie jestem C ++ programistą , więc nie będę tu pisać żadnego kodu, ale jak dotąd nikt nie wspomniał o rozwiązaniu zorientowanym obiektowo. Oto moje przypuszczenie:

Mieć ogólny interfejs, który zapewnia metodę oceny pojedynczego warunku. Teraz możesz użyć listy implementacji tych warunków w swoim obiekcie zawierającej przedmiotową metodę. Powtarzasz listę i oceniasz każdy warunek, być może zerwanie wcześnie, jeśli jeden zawiedzie.

Dobrą rzeczą jest to, że taki projekt bardzo dobrze trzyma się zasady otwarta / zamknięta , ponieważ można łatwo dodać nowe warunki podczas inicjalizacji obiektu zawierającego daną metodę. Możesz nawet dodać drugą metodę do interfejsu za pomocą metody oceny stanu, zwracając opis warunku. Można to wykorzystać w systemach samodokumentujących.

Minusem jest jednak to, że wiąże się to z nieco większym obciążeniem z powodu użycia większej liczby obiektów i iteracji na liście.

SpaceTrucker
źródło
Czy możesz dodać przykład w innym języku? Myślę, że to pytanie dotyczy wielu języków, chociaż zostało zadane konkretnie o C ++.
Denise Skidmore,
1

Tak to robię.

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}
Vadoff
źródło
1

Po pierwsze, krótki przykład pokazujący, dlaczego gotonie jest dobrym rozwiązaniem dla C ++:

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

Spróbuj skompilować to do pliku obiektowego i zobacz, co się stanie. Następnie wypróbuj ekwiwalent do+ break+while(0) .

To było na boku. Główny punkt jest następujący.

Te małe fragmenty kodu często wymagają pewnego rodzaju czyszczenia w przypadku awarii całej funkcji. Te porządki zwykle chcą się odbywać w odwrotnej kolejności niż same części, ponieważ „odprężasz” częściowo ukończone obliczenia.

Jedną z opcji uzyskania tej semantyki jest RAII ; patrz odpowiedź @ utnapistim. C ++ gwarantuje, że automatyczne niszczyciele działają w odwrotnej kolejności do konstruktorów, co naturalnie zapewnia „odwijanie”.

Ale to wymaga wielu klas RAII. Czasami prostszą opcją jest użycie stosu:

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

...i tak dalej. Jest to łatwe do skontrolowania, ponieważ umieszcza kod „cofnij” obok kodu „do”. Łatwa kontrola jest dobra. Sprawia również, że kontrola jest bardzo wyraźna. Jest to również użyteczny wzór dla C.

Może wymagać, aby calcfunkcje przyjmowały wiele argumentów, ale zwykle nie stanowi to problemu, jeśli twoje klasy / struktury mają dobrą spójność. (Oznacza to, że rzeczy, które należą do siebie, żyją w jednym obiekcie, więc te funkcje mogą pobierać wskaźniki lub odniesienia do niewielkiej liczby obiektów i nadal wykonywać wiele przydatnych prac.)

Nemo
źródło
Bardzo łatwo jest skontrolować ścieżkę oczyszczania, ale może nie tak łatwo prześledzić złotą ścieżkę. Ale ogólnie myślę, że coś takiego zachęca do spójnego wzorca oczyszczania.
Denise Skidmore,
0

Jeśli kod ma długi blok instrukcji if..else if..else, możesz spróbować przepisać cały blok za pomocą Functorslub function pointers. Może nie zawsze jest to właściwe rozwiązanie, ale często tak jest.

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html

HAL
źródło
Zasadniczo jest to możliwe (i nie jest złe), ale z jawnymi obiektami funkcji lub wskaźnikami po prostu zbyt mocno zakłócają przepływ kodu. OTOH, tutaj równoważne, użycie lambdas lub zwykłych nazwanych funkcji jest dobrą praktyką, wydajną i dobrze czyta.
leftaroundabout
0

Jestem zdumiony liczbą prezentowanych tutaj różnych odpowiedzi. Ale w końcu w kodzie, który muszę zmienić (tj. Usunąć todo-while(0) hack lub cokolwiek innego), zrobiłem coś innego niż którakolwiek z wymienionych tu odpowiedzi i jestem zdezorientowany, dlaczego nikt o tym nie pomyślał. Oto co zrobiłem:

Kod początkowy:

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

Teraz:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

To, co zostało tutaj zrobione, polega na tym, że wykańczanie zostało wyizolowane w funkcji, a rzeczy nagle stały się takie proste i czyste!

Myślałem, że warto wspomnieć o tym rozwiązaniu, więc podam je tutaj.

Sankalp
źródło
0

Skonsoliduj to w jedno ifoświadczenie:

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

Jest to wzorzec używany w językach takich jak Java, w których usunięto słowo kluczowe goto.

Tyzoid
źródło
2
Działa to tylko wtedy, gdy nie trzeba wykonywać żadnych czynności między testami warunków. (no cóż, przypuszczam, że można ukryć robienie rzeczy wewnątrz niektórych wywołań funkcji wykonanych przez testy warunków, ale może to być nieco zaciemnione, jeśli zrobisz to za dużo)
Jeremy Friesner
@JeremyFriesner W rzeczywistości można robić rzeczy pośrednie jako osobne funkcje boolowskie, które zawsze oceniają jako true. Ocena zwarć zagwarantuje, że nigdy nie będziesz uruchamiać między tymi czynnościami, dla których wszystkie wymagane testy wstępne nie przeszły.
AJMansfield
@AJMansfield tak, o tym mówiłem w drugim zdaniu ... ale nie jestem pewien, czy to poprawiłoby jakość kodu.
Jeremy Friesner,
@JeremyFriesner Nic nie stoi na przeszkodzie, aby napisać warunki, ponieważ (/*do other stuff*/, /*next condition*/)możesz nawet ładnie je sformatować. Tylko nie oczekuj, że ludzie to polubią. Ale szczerze mówiąc, to tylko pokazuje, że błędem dla Javy było wycofanie tego gotooświadczenia ...
cmaster - przywrócenie moniki
@JeremyFriesner Zakładałem, że były to booleany. Jeśli funkcje mają być uruchamiane wewnątrz każdego warunku, istnieje lepszy sposób, aby sobie z tym poradzić.
Tyzoid
0

Jeśli używasz tej samej procedury obsługi błędów dla wszystkich błędów, a każdy krok zwraca wartość logiczną wskazującą powodzenie:

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(Podobne do odpowiedzi tyzoida, ale warunki są działaniami, a && zapobiega wystąpieniu dodatkowych działań po pierwszej awarii).

Denise Skidmore
źródło
0

Dlaczego nie zgłoszono metody oznaczania, jest używana od wieków.

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
Fennekin
źródło