Try-catch lub ifs do obsługi błędów w C ++

30

Czy wyjątki są szeroko stosowane w projektowaniu silnika gry, czy bardziej wskazane jest stosowanie czystych instrukcji if? Na przykład z wyjątkami:

try {
    m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
    m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
    m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
    m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
    m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
    m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
    // some info about error
}

i jeśli:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
    // show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
    // show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
    // show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
    // show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
    // show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
    // show error
}

Słyszałem, że wyjątki są nieco wolniejsze niż ifs i nie powinienem mieszać wyjątków metodą sprawdzania if. Jednak z wyjątkami mam bardziej czytelny kod niż z mnóstwem ifs po każdej metodzie initialize () lub czymś podobnym, chociaż moim zdaniem są one czasem zbyt ciężkie, by zmieścić tylko jedną metodę. Czy nadają się do tworzenia gier, czy lepiej trzymać się prostych instrukcji if?

Tobi
źródło
Wiele osób twierdzi, że Boost jest szkodliwy dla rozwoju gier, ale polecam spojrzeć na [„Boost.optional”] ( boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html ) twój przykład ifs jest trochę ładniejszy
ManicQin 30.12.12
Jest miła rozmowa na temat obsługi błędów przez Andrei Alexandrescu, zastosowałem podobne podejście do jego bez wyjątków.
Thelvyn

Odpowiedzi:

48

Krótka odpowiedź: przeczytaj: Rozsądna obsługa błędów 1 , Rozsądna obsługa błędów 2 i Rozsądna obsługa błędów 3 autorstwa Niklas Frykholm. W rzeczywistości czytaj wszystkie artykuły na tym blogu, gdy jesteś przy nim. Nie twierdzę, że zgadzam się ze wszystkim, ale większość to złoto.

Nie używaj wyjątków. Istnieje wiele powodów. Wymienię najważniejsze.

Mogą być rzeczywiście wolniejsze, choć jest to nieco zminimalizowane w nowszych kompilatorach. Niektóre kompilatory obsługują „wyjątki narzutu zerowego” dla ścieżek kodu, które w rzeczywistości nie wyzwalają wyjątku (choć to trochę kłamstwo, ponieważ wciąż istnieją dodatkowe dane, których wymaga obsługa wyjątków, zwiększając rozmiar pliku wykonywalnego / dll). Rezultatem końcowym jest to, że tak, stosowanie wyjątków jest wolniejsze i na każdej ścieżce kodu krytycznej pod względem wydajności należy absolutnie ich unikać. W zależności od kompilatora włączenie ich w ogóle może zwiększyć obciążenie. Zawsze zawsze zwiększają rozmiar kodu, w wielu przypadkach dość znacząco, co może poważnie wpłynąć na wydajność dzisiejszego sprzętu.

Wyjątki sprawiają, że kod jest znacznie bardziej kruchy. Istnieje niesławna grafika (której niestety nie mogę teraz znaleźć), która po prostu pokazuje na wykresie trudność pisania kodu bezpiecznego dla wyjątku w porównaniu do kodu niebezpiecznego dla wyjątku, a ten pierwszy jest znacznie większym paskiem. Jest po prostu wiele małych błędów z wyjątkami i wiele sposobów pisania kodu, który wygląda na wyjątkowo bezpieczny, ale tak naprawdę nie jest. Nawet cały komitet C ++ 11 wygłupił się i zapomniał dodać ważne funkcje pomocnicze do prawidłowego używania std :: unique_ptr, a nawet przy tych funkcjach pomocniczych potrzeba więcej pisania niż ich użycie, a większość programistów wygrała ” nawet zdają sobie sprawę, co jest nie tak, jeśli nie.

W szczególności dla branży gier, kompilatory / środowiska wykonawcze dostarczone przez niektórych konsol wprost nie obsługują wyjątków w pełni, a nawet nie obsługują ich wcale. Jeśli Twój kod korzysta z wyjątków, być może nadal w tym dniu musisz przepisać fragmenty kodu, aby przenieść go na nowe platformy. (Nie jestem pewien, czy zmieni się to w ciągu 7 lat od wydania konsol; po prostu nie używamy wyjątków, wyłączamy je nawet w ustawieniach kompilatora, więc nie wiem, czy ktoś, z kim rozmawiam, ostatnio sprawdził).

Ogólna linia myślenia jest dość jasna: należy stosować wyjątki w wyjątkowych okolicznościach. Użyj ich, gdy twój program wpadnie w „Nie mam pojęcia, co robić, może mam nadzieję, że zrobi to ktoś inny, więc rzucę wyjątek i zobaczę, co się stanie”. Używaj ich, gdy żadna inna opcja nie ma sensu. Używaj ich, gdy nie przejmujesz się, że przypadkowo wycieknie trochę pamięci lub nie wyczyścisz zasobu, ponieważ pomieszałeś użycie odpowiednich inteligentnych uchwytów. We wszystkich innych przypadkach nie używaj ich.

Jeśli chodzi o kod jak twój przykład, masz kilka innych sposobów rozwiązania problemu. Jednym z bardziej niezawodnych - choć niekoniecznie najbardziej idealnych w twoim prostym przykładzie - jest skłanianie się ku typom błędów monadycznych. Oznacza to, że createText () może zwrócić niestandardowy typ uchwytu zamiast liczby całkowitej. Ten typ uchwytu ma akcesoria do aktualizacji lub kontrolowania tekstu. Jeśli uchwyt zostanie przełączony w stan błędu (ponieważ nie powiodło się wywołanie metody createText ()), dalsze wywołania uchwytu po prostu zakończą się niepowodzeniem. Możesz także wysłać zapytanie do uchwytu, aby sprawdzić, czy jest on błędny, a jeśli tak, jaki był ostatni błąd. To podejście ma więcej kosztów ogólnych niż inne opcje, ale jest dość solidne. Użyj go w przypadkach, gdy musisz wykonać długi ciąg operacji w kontekście, w którym jakakolwiek pojedyncza operacja może się nie powieść w produkcji, ale gdzie nie możesz / nie możesz / wygrałeś ”

Alternatywą dla implementacji monadycznej obsługi błędów jest zamiast używania niestandardowych obiektów uchwytów, aby metody w obiekcie kontekstowym w sposób łagodny radziły sobie z nieprawidłowymi identyfikatorami uchwytów. Na przykład, jeśli createText () zwraca -1, gdy zawiedzie, to wszelkie inne wywołania m_statistics, które przyjmują jeden z tych uchwytów, powinny z wdziękiem wyjść, jeśli -1 zostanie przekazane.

Możesz również umieścić błąd drukowania w funkcji, która faktycznie nie działa. W twoim przykładzie, createText () prawdopodobnie ma znacznie więcej informacji o tym, co poszło źle, więc będzie mógł zrzucić bardziej znaczący błąd do dziennika. W tym przypadku nie ma większych korzyści z przekazywania obsługi błędów / drukowania do dzwoniących. Zrób to, gdy dzwoniący będą musieli dostosować obsługę (lub użyć wstrzykiwania zależności). Pamiętaj, że posiadanie konsoli w grze, która może się pojawiać po zarejestrowaniu błędu, jest dobrym pomysłem i również tutaj pomaga.

Najlepszą opcją (już przedstawioną w linkowanej serii artykułów powyżej) dla wywołań, których nie spodziewasz się w żadnym zdrowym środowisku - jak zwykły akt tworzenia obiektów blob tekstowych dla systemu statystyk - jest po prostu funkcja to się nie powiodło (w twoim przykładzie createText) się przerywa. Możesz być całkiem pewny, że createText () nie zawiedzie w produkcji, chyba że coś zostanie całkowicie wyeliminowane (np. Użytkownik usunął pliki danych czcionek lub z jakiegoś powodu ma tylko 256 MB pamięci itp.). W wielu z tych przypadków nic dobrego nie można zrobić, gdy dojdzie do awarii. Brak pamięci? Możesz nawet nie być w stanie dokonać alokacji niezbędnej do utworzenia ładnego panelu GUI, aby pokazać użytkownikowi błąd OOM. Brakuje czcionek? Utrudnia wyświetlanie użytkownikowi błędów. Cokolwiek jest nie tak,

Samo zawieszanie się jest w porządku, o ile (a) logujesz błąd do pliku dziennika i (b) robisz to tylko w przypadku błędów, które nie są spowodowane zwykłymi działaniami użytkownika.

Nie powiedziałbym wcale tego samego w przypadku wielu aplikacji serwerowych, w których dostępność jest krytyczna, a monitorowanie watchdoga nie jest wystarczające, ale jest to zupełnie inne niż tworzenie klienta gry . Chciałbym również zdecydowanie unikać używania C / C ++, ponieważ narzędzia do obsługi wyjątków innych języków nie gryzą podobnie jak C ++, ponieważ są to środowiska zarządzane i nie mają wszystkich problemów związanych z bezpieczeństwem wyjątków, jakie ma C ++. Wszelkie problemy z wydajnością są również łagodzone, ponieważ serwery koncentrują się bardziej na równoległości i przepustowości niż na minimalnych opóźnieniach, takich jak klienci gier. Na przykład nawet serwery gier akcji dla strzelców i tym podobne mogą działać całkiem dobrze, gdy są napisane w języku C #, ponieważ rzadko przekraczają granice sprzętu, jak to zwykle robią klienci FPS.

Sean Middleditch
źródło
1
+1. Ponadto istnieje możliwość dodania atrybutu, takiego jak warn_unused_resultfunkcja w niektórych kompilatorach, co pozwala przechwycić nieobsługiwany kod błędu.
Maciej Piechotka
Przybyłem tutaj, widząc C ++ w tytule, i byłem całkowicie pewien, że obsługa błędów monadycznych już tu nie będzie. Dobry towar!
Adam
co z miejscami, w których wydajność nie jest krytyczna, a Twoim celem jest przede wszystkim szybkie wdrożenie? na przykład kiedy wdrażasz logikę gry?
Ali1S232,
a co z pomysłem użycia obsługi wyjątków tylko w celu debugowania? tak jak po wydaniu gry oczekujesz, że wszystko przebiegnie bezproblemowo. więc używasz wyjątków, aby znaleźć i naprawić błędy podczas programowania, a później w trybie wydania usuń wszystkie te wyjątki.
Ali1S232
1
@Gajoo: w przypadku logiki gry wyjątki po prostu utrudniają przestrzeganie logiki (ponieważ utrudnia to przestrzeganie całego kodu). Z mojego doświadczenia wynika, że ​​nawet w Pythnn i C # wyjątki są rzadkie w logice gry. w przypadku debugowania twarde stwierdzenie jest zwykle znacznie wygodniejsze. Przerwij i zatrzymaj dokładnie w tym momencie, gdy coś pójdzie nie tak, nie po rozwinięciu dużych części stosu i utracie wszelkiego rodzaju informacji kontekstowych z powodu semantyki wyrzucania wyjątków. Do logiki debugowania chcesz budować narzędzia i GUI do informacji / edycji, aby projektanci mogli sprawdzać i poprawiać.
Sean Middleditch,
13

Stara mądrość samego Donalda Knutha to:

„Powinniśmy zapomnieć o niewielkiej nieefektywności, powiedzmy w około 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła”.

Wydrukuj to jako duży plakat i powiesz go wszędzie, gdzie wykonujesz poważne programowanie.

Więc nawet jeśli try / catch jest trochę wolniejszy, użyłbym go domyślnie:

  • Kod musi być jak najbardziej czytelny i zrozumiały. Państwo może napisać kod raz, ale będzie ją przeczytać wiele razy więcej do debugowania tego kodu, do debugowania inny kod, dla zrozumienia, w celu poprawy ...

  • Poprawność w stosunku do wydajności. Prawidłowe wykonywanie czynności if / else nie jest trywialne. W twoim przykładzie jest to zrobione niepoprawnie, ponieważ nie można pokazać jednego błędu, ale wiele. Musisz użyć kaskadowo, jeśli / to / else.

  • Łatwiejsze w użyciu konsekwentnie: Try / catch obejmuje styl, w którym nie trzeba sprawdzać błędów w każdej linii. Z drugiej strony: brakuje jednego, jeśli / else i twój kod może zniszczyć spustoszenie. Oczywiście tylko w nie powtarzalnych okolicznościach.

Więc ostatni punkt:

Słyszałem, że wyjątki są nieco wolniejsze niż ifs

Słyszałem to około 15 lat temu, ostatnio nie słyszałem o tym z żadnych wiarygodnych źródeł. Kompilatory mogły ulec poprawie lub cokolwiek innego.

Najważniejsze jest to: nie przeprowadzaj przedwczesnej optymalizacji. Zrób to tylko wtedy, gdy można udowodnić, że kod odniesienia pod ręką jest mocno wewnętrzna pętla pewnym stopniu wykorzystane kodu i że styl przełączania poprawia wydajność znacznie . To jest 3% przypadek.

AH
źródło
22
Zrobię to -1 do nieskończoności. Nie mogę pojąć szkody, jaką ten cytat Knuth wyrządził mózgom programistów. Biorąc pod uwagę, czy użycie wyjątków nie jest przedwczesną optymalizacją, jest to decyzja projektowa, ważna decyzja projektowa, która ma konsekwencje znacznie wykraczające poza wydajność.
sam hocevar
3
@SamHocevar: Przykro mi, ale nie rozumiem: Pytanie dotyczy możliwego spadku wydajności przy zastosowaniu wyjątków. Moja uwaga: nie myśl o tym, hit nie jest taki zły (jeśli w ogóle), a inne rzeczy są o wiele ważniejsze. Tutaj wydaje się, że zgadzasz się, dodając „decyzję projektową” do mojej listy. DOBRZE. Z drugiej strony mówisz, że cytat Knutha jest zły, co oznacza, że ​​przedwczesna optymalizacja nie jest zła. Ale dokładnie tak się stało tutaj IMO: Q nie myśli o architekturze, projekcie lub różnych algorytmach, tylko o wyjątkach i ich wpływie na wydajność.
AH,
2
Nie zgadzałbym się z jasnością w C ++. C ++ mają niezaznaczone wyjątki, podczas gdy niektóre kompilatory implementują sprawdzone wartości zwracane. W rezultacie masz kod, który wygląda wyraźniej, ale może mieć ukryty wyciek pamięci lub nawet wprowadzić kod w stan nieokreślony. Również kod innej firmy może nie być wyjątkowo bezpieczny. (Sytuacja wygląda inaczej w języku Java / C #, w którym sprawdzono wyjątki, GC, ...). Od decyzji projektowej - jeśli nie przekroczą punktów API, refaktoryzacja z / do każdego stylu może być wykonana półautomatycznie za pomocą Perla One-Liner.
Maciej Piechotka
1
@SamHocevar: „ Pytanie dotyczy tego, co jest lepsze, a nie co jest szybsze. ” Ponownie przeczytaj ostatni akapit pytania. Jedyny powód, dla którego jest nawet myśleć o użyciu wyjątków nie jest, bo myśli, że może być wolniejszy. Teraz, jeśli uważasz, że istnieją inne obawy, których OP nie wziął pod uwagę, możesz je opublikować lub głosować za tymi, którzy to zrobili. Ale PO bardzo wyraźnie koncentruje się na realizacji wyjątków.
Nicol Bolas,
3
@AH - jeśli chcesz zacytować Knutha, nie zapomnij o pozostałej części (i najważniejszej części IMO): „ Jednak nie powinniśmy tracić naszych możliwości w tak krytycznych 3%. Dobry programista nie będzie uspokojony takim rozumowaniem rozsądnie przyjrzy się krytycznemu kodowi, ale dopiero po zidentyfikowaniu tego kodu ”. Zbyt często postrzega się to jako wymówkę, aby w ogóle nie optymalizować lub próbować uzasadnić powolność kodu w przypadkach, gdy wydajność nie jest optymalizacją, ale w rzeczywistości jest podstawowym wymogiem.
Maximus Minimus
10

Odpowiedź na to pytanie była już naprawdę świetna, ale chciałbym dodać kilka przemyśleń na temat czytelności kodu i odzyskiwania błędów w konkretnym przypadku.

Uważam, że Twój kod może wyglądać tak:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );

Bez wyjątków, bez ifs. Raportowanie błędów można wykonać w createText. I createTextmoże zwrócić domyślny identyfikator tekstury, który nie wymaga sprawdzania wartości zwracanej, dzięki czemu reszta kodu działa równie dobrze.

sam hocevar
źródło