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!
goto
- ale jestem pewien, że ktoś oceni mnie za sugestię, więc nie piszę odpowiedzi na ten temat. DŁUGOdo ... while(0);
wydaje się niewłaściwą rzeczą.goto
, bądź szczery i rób to na zewnątrz, nie ukrywaj go za pomocąbreak
ido ... while
goto
wysiłek rehabilitacyjny. :)Odpowiedzi:
Uznaje się za dopuszczalną praktykę izolowanie tych decyzji w funkcji i stosowanie
return
s zamiastbreak
s. Chociaż wszystkie te kontrole odpowiadają temu samemu poziomowi abstrakcji co funkcja, jest to dość logiczne podejście.Na przykład:
źródło
return
jest czystszy, ponieważ każdy czytelnik natychmiast wie, że działa poprawnie i co robi. Zegoto
trzeba się rozejrzeć, aby zobaczyć, co to jest za, i mieć pewność, że nie popełniono błędy. Przebranie jest zaletą.forceinline
.Są chwile, kiedy używanie
goto
jest w rzeczywistości PRAWĄ odpowiedzią - przynajmniej dla tych, którzy nie wychowali się w przekonaniu religijnym, że „goto
nigdy 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ęgoto
zabreak
. 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:
Może sprawić, że JEST CZYSTSZY, że kod jest poprawny, ponieważ nie ma tak złożonej logiki.
Edycja: Aby wyjaśnić, w żadnym wypadku nie proponuję zastosowania
goto
jako ogólnego rozwiązania. Ale są przypadki, w którychgoto
jest 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.
źródło
goto
może mieć miejsce, alegoto cleanup
nie ma. Czyszczenie odbywa się za pomocą RAII.goto
nigdy nie jest właściwą odpowiedzią. Byłbym wdzięczny, gdyby pojawił się komentarz ...goto
ponieważ musisz pomyśleć, aby go użyć / zrozumieć program, który go używa ... Z drugiej strony mikroprocesory są zbudowanejumps
iconditional jumps
... więc problem dotyczy niektórych osób, a nie logiki lub czegoś innego.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.Możesz użyć prostego wzorca kontynuacji ze
bool
zmienną:Ten łańcuch wykonania zatrzyma się, gdy tylko
checkN
zwróci afalse
. Żadne dalszecheck...()
połączenia nie będą wykonywane z powodu zwarcia&&
operatora. Ponadto optymalizacja kodu wynikowego są wystarczająco inteligentny, aby uznać, że ustawieniegoOn
nafalse
to ulica jednokierunkowa i wstawić brakującegoto end
dla Ciebie. W rezultacie wydajność powyższego kodu byłaby identyczna z wydajnością ado
/while(0)
, tylko bez bolesnego pogorszenia jego czytelności.źródło
if
warunkach wewnętrznych wyglądają bardzo podejrzanie.if
bez względu na to, co się stanie,goOn
nawet 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()
tocheckN()
zawsze będzie działał, nawet jeśligoOn
byłfalse
na początkuif
(tzn. nie rób tego).if
.Spróbuj wyodrębnić kod do oddzielnej funkcji (lub więcej niż jednej). Następnie powróć z funkcji, jeśli sprawdzenie się nie powiedzie.
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
break
zreturn
(lubthrow
, jeżeli jest to bardziej właściwe) i niech destructor oczyścić obiektu dla Ciebie.Jeśli przepływ programu jest (koniecznie) tak zawijasowy, że naprawdę potrzebujesz go
goto
, użyj go zamiast dawać dziwne przebranie.Jeśli masz reguły kodowania, które na ślepo zabraniają
goto
i naprawdę nie możesz uprościć przepływu programu, prawdopodobnie będziesz musiał ukryć to za pomocądo
hacka.źródło
goto
czy to naprawdę lepsza opcja.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:
W C, w większości codebases The
if(error_ok != ...
agoto
kod jest zwykle ukryte za niektórych makr (convenienceRET(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
cleanup
bloku 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 ++:
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)
konstrukcjiMoż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łemdo { ... } while(0)
nowy kod, ani#define
makra opakowania goto i etykiety czyszczenia, ani coś jeszcze).źródło
shared_ptr
z niestandardowym urządzeniem do usuwania może zrobić wiele rzeczy. Jeszcze łatwiejsze dzięki lambdom w C ++ 11.namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };
W tym przypadku (zmake_handle
ustawieniem poprawnego typu usuwacza podczas budowy) nazwa typu nie sugeruje, że jest to wskaźnik .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 jestgoto
w przebraniu ...źródło
Na pewno nie odpowiedź, ale odpowiedź (w trosce o kompletność)
Zamiast :
Możesz napisać:
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
I używaj go jak
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.
źródło
define BLOCK switch (0) case 0:
i używać go jakBLOCK { ... break; }
.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:źródło
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.func()
i umożliwieniu jego niszczycielowi obsługi uwolnienia zasobów? Jeśli cokolwiek pozafunc()
dostępem wymaga dostępu do tego samego zasobu, powinno zostać zadeklarowane na stercie przed wywołaniemfunc()
przez odpowiedniego menedżera zasobów.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.
źródło
Jeśli nie musisz wprowadzać zmiennych lokalnych podczas wykonywania, często możesz spłaszczyć to:
źródło
Podobne do odpowiedzi dasblinkenlight, ale unika przypisania wewnątrz,
if
które może zostać „naprawione” przez recenzenta kodu:...
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.źródło
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”? .
źródło
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.Dla mnie
do{...}while(0)
jest w porządku. Jeśli nie chceszdo{...}while(0)
, możesz zdefiniować dla nich alternatywne słowa kluczowe.Przykład:
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
goto
również zapewnia czystszy kod i jest to proste dzięki logice warunku, a następnie skoku. Możesz wykonać następujące czynności:Uwaga: etykieta jest umieszczana po zamknięciu
}
. Jest to uniknięcie jednego możliwego problemugoto
polegającego na przypadkowym umieszczeniu kodu pośredniego, ponieważ etykieta nie była widoczna. Jest teraz jakdo{...}while(0)
bez kodu warunkowego.Aby uczynić ten kod czystszym i bardziej zrozumiałym, możesz to zrobić:
Dzięki temu możesz wykonywać zagnieżdżone bloki i określać, gdzie chcesz wyjść / wyskoczyć.
Kod spaghetti to nie wina
goto
, to wina programisty. Nadal możesz produkować kod spaghetti bez użyciagoto
.źródło
goto
ponad rozszerzenie składni języka za pomocą preprocesora milion razy.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:
Każdy krok obliczeniowy może oczywiście zrobić coś takiego jak return,
boost::none
jeśli podany opcjonalny jest pusty. Na przykład:Następnie połącz je razem:
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.
źródło
optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);
zamiast tego użyć funkcji kompozycji monadycznej („bind”) zamiast aplikacji funkcji.Co powiesz na przeniesienie instrukcji if do dodatkowej funkcji dającej wynik liczbowy lub wyliczeniowy?
źródło
Być może coś takiego
lub użyj wyjątków
Korzystając z wyjątków, możesz również przekazywać dane.
źródło
ever
będzie bardzo niezadowolony ...Nie przepadam za używaniem
break
anireturn
w 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,
Patrząc na otaczające bloki, łatwo jest stwierdzić, że
myLogic()
dzieje się to tylko wtedy, gdyconditionA and conditionB and conditionC
jest to prawdą.Staje się znacznie mniej widoczny, gdy pojawiają się wczesne zwroty:
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:
(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, kiedyisB && conditionC
. Czytelnik otrzymuje wskazówkę, że musi dalej sprawdzać, co spowoduje, że B jest prawdą.źródło
Co ty na to?
źródło
return condition;
, w przeciwnym razie uważam, że jest to łatwe do utrzymania.Kolejny wzór przydatny, jeśli potrzebujesz różnych kroków czyszczenia w zależności od tego, gdzie jest awaria:
źródło
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.
źródło
Tak to robię.
źródło
Po pierwsze, krótki przykład pokazujący, dlaczego
goto
nie jest dobrym rozwiązaniem dla C ++: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:
...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
calc
funkcje 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.)źródło
Jeśli kod ma długi blok instrukcji if..else if..else, możesz spróbować przepisać cały blok za pomocą
Functors
lubfunction 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
źródło
Jestem zdumiony liczbą prezentowanych tutaj różnych odpowiedzi. Ale w końcu w kodzie, który muszę zmienić (tj. Usunąć to
do-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:
Teraz:
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.
źródło
Skonsoliduj to w jedno
if
oświadczenie:Jest to wzorzec używany w językach takich jak Java, w których usunięto słowo kluczowe goto.
źródło
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.(/*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 tegogoto
oświadczenia ...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:
(Podobne do odpowiedzi tyzoida, ale warunki są działaniami, a && zapobiega wystąpieniu dodatkowych działań po pierwszej awarii).
źródło
Dlaczego nie zgłoszono metody oznaczania, jest używana od wieków.
źródło