Jak ważne jest dla Ciebie bezpieczeństwo wyjątków w kodzie C ++?


19

Za każdym razem, gdy zastanawiam się, czy mój kod jest wyjątkowo bezpieczny, uzasadniam, że tego nie robię, ponieważ byłoby to tak czasochłonne. Rozważ ten stosunkowo prosty fragment:

Level::Entity* entity = new Level::Entity();
entity->id = GetNextId();
entity->AddComponent(new Component::Position(x, y));
entity->AddComponent(new Component::Movement());
entity->AddComponent(new Component::Render());
allEntities.push_back(entity); // std::vector
entityById[entity->id] = entity; // std::map
return entity;

Aby wdrożyć podstawową gwarancję wyjątku, mógłbym użyć wskaźnika zasięgu w newpołączeniach. Zapobiegnie to wyciekom pamięci, jeśli którekolwiek z wywołań zgłosi wyjątek.

Powiedzmy jednak, że chcę wdrożyć silną gwarancję wyjątku. Przynajmniej musiałbym zaimplementować wspólny wskaźnik dla moich kontenerów (nie używam Boost), nothrow Entity::Swapdo dodawania atomów atomowo i jakiś idiom do atomowego dodawania zarówno do, jak Vectori Map. Wdrożenie byłoby nie tylko czasochłonne, ale także kosztowne, ponieważ wymaga znacznie więcej kopiowania niż rozwiązanie niebezpieczne.

Ostatecznie wydaje mi się, że czas spędzony na robieniu tego wszystkiego nie byłby usprawiedliwiony tylko dlatego, że prosta CreateEntityfunkcja jest wyjątkowo bezpieczna. Prawdopodobnie chcę po prostu wyświetlić błąd i zamknąć go w tym momencie.

Jak daleko posuwasz się w swoich własnych projektach gier? Czy ogólnie dopuszczalne jest pisanie niebezpiecznego kodu wyjątku dla programu, który może ulec awarii, gdy wystąpi wyjątek?


21
Kiedy w przeszłości pracowałem nad projektami C ++, udawaliśmy, że wyjątki nie istnieją.
Tetrad

2
Ja osobiście uwielbiam wyjątki i często naprawdę lubię wymyślać, jak sprawić, by mój kod zapewniał silne gwarancje. Jeśli jednak zawiedziesz się, gdy otrzymasz wyjątek ... może nie przejmuj się nim.

7
Osobiście używam wyjątków do zarządzania… hm… „wyjątkami”, a nie błędami. Na przykład, jeśli wiem, że funkcja może ulec awarii, pozwolę jej ulec awarii, ale jeśli wiem, że funkcja może nie odczytać archiwum, ale jest inny sposób, otaczam ją próbą złapania. a jeśli jest to wyjątek „niezdolny do odczytu”, po prostu zarządzam nim w inny sposób, tak jak zrobiłbym to bez odczytu archiwum.
Gustavo Maciel

To, co powiedział Gtoknu, musisz wiedzieć, jakie wyjątki mogą wnieść dowolne zewnętrzne biblioteki.
Peter Ølsted

Odpowiedzi:


26

Jeśli zamierzasz korzystać z wyjątków (co nie oznacza, że ​​powinieneś, istnieją zalety i wady tego podejścia, które nie są objęte zakresem tego pytania), powinieneś napisać odpowiednio bezpieczny kod wyjątku.

Kod, który nie korzysta z wyjątków i wyraźnie nie jest bezpieczny jako wyjątek, jest lepszy niż kod, który ich używa, ale w połowie zapewnia bezpieczeństwo wyjątkowe. Jeśli masz ten drugi przypadek, masz całą klasę błędów i awarii, które mogą wystąpić, gdy zdarzy się wyjątek, z którego prawdopodobnie nie będziesz w stanie odzyskać, co czyni jedną z korzyści wyjątków całkowicie nieważną. Nawet jeśli jest to stosunkowo rzadka klasa awarii, nadal jest możliwa.

Nie oznacza to, że jeśli używasz wyjątków, cały kod musi zapewniać silną gwarancję wyjątku - wystarczy, że każdy fragment kodu powinien zapewniać jakąś gwarancję (brak, podstawową, silną), aby podczas konsumpcji tego kodu wiesz, co gwarantuje Ci konsument. Zasadniczo im niższy poziom danego elementu, tym silniejsza powinna być gwarancja.

Jeśli nigdy nie zapewnisz silnej gwarancji lub nigdy naprawdę nie zaakceptujesz paradygmatów obsługi wyjątków oraz wszystkich dodatkowych prac i wad, które się z tym wiążą, to tak naprawdę nie uzyskasz wszystkich korzyści z tego wynikających i możesz mieć łatwiej czas po prostu rezygnuje z wyjątków.


12
To samo jest warte +1: „Kod, który nie korzysta z wyjątków i wyraźnie nie jest bezpieczny jako wyjątek, jest lepszy niż kod, który ich używa, ale w połowie zapewnia bezpieczeństwo wyjątkowe”.
Maximus Minimus

14

Moja ogólna zasada: jeśli chcesz użyć wyjątków, użyj wyjątków. Jeśli nie musisz używać wyjątków, nie używaj wyjątków. Jeśli nie wiesz, czy musisz użyć wyjątków, nie musisz ich używać.

Wyjątki stanowią ostatnia linia obrony w zawsze krytycznych aplikacjach. Jeśli piszesz oprogramowanie do kontroli ruchu lotniczego, które absolutnie nigdy nie może zawieść, skorzystaj z wyjątków. Jeśli piszesz kod kontrolny dla elektrowni jądrowej, użyj wyjątków.

Z drugiej strony, przy opracowywaniu gry lepiej jest postępować zgodnie z mantrą: Crash wcześnie, głośno crash. Jeśli jest jakiś problem, naprawdę chcesz o tym wiedzieć jak najszybciej; nigdy nie chcesz, aby błędy były „niewidocznie” usuwane, ponieważ często ukrywa to rzeczywistą przyczynę późniejszych niewłaściwych zachowań i sprawia, że ​​debugowanie jest znacznie trudniejsze niż to konieczne.


5

Problem z wyjątkami polega na tym, że musisz sobie z nimi poradzić. Jeśli otrzymujesz wyjątek z powodu braku pamięci, być może i tak nie będziesz w stanie poradzić sobie z tym błędem. Równie dobrze może po prostu zrzucić całą grę w 1 gigantycznym programie obsługi wyjątków, który wyskakuje z okienkiem błędu.

Przy tworzeniu gier wolę próbować znaleźć sposoby, aby mój kod łaskawie kontynuował awarie, jeśli to możliwe.

Na przykład w mojej grze na stałe zakoduję specjalną siatkę ERROR. To obracający się czerwony okrąg z białym X i białą obwódką. W zależności od gry niewidzialna może być lepsza. Ma jedną instancję singletona zainicjowaną przy starcie. Ponieważ jest zapisany w kodzie zamiast używania plików zewnętrznych, nie powinno być możliwe niepowodzenie.

W przypadku, gdy normalne wywołanie loadMesh nie powiedzie się, zamiast wyrzucać błąd i zawieszać się na pulpicie, po cichu rejestruje awarię w pliku konsoli / logu i zwraca siatkę błędów na stałe. Dowiedziałem się, że Skyrim robi to samo, gdy mod spieprzył sprawę. Istnieje duża szansa, że ​​gracz nawet nie zobaczy siatki błędów (chyba że istnieje jakiś poważny problem, a wielu z nich jest podobnych).

Istnieje również moduł cieniujący. Taki, który wykonuje podstawowe operacje na macierzy wierzchołków, ale jeśli z jakiegoś powodu nie ma jednolitej macierzy, powinien nadal wykonywać widok ortogonalny. Nie ma odpowiedniego cieniowania / oświetlenia / materiałów (chociaż ma flagę color_overide, dzięki czemu mogę go używać z siatką błędów).

Jedynym możliwym problemem jest to, czy wersja siatki błędów może trwale zepsuć grę. Na przykład upewnij się, że nie stosuje fizyki, ponieważ jeśli gracz ładuje obszar, wtedy siatka błędu kopnięcia w nim może spaść na podłogę, a jeśli ponownie załadują grę z działającą prawidłową siatką, jeśli jest większa, może to być osadzony w ziemi. Nie powinno to być zbyt trudne, ponieważ prawdopodobnie zamierzasz przechowywać siatki fizyczne z graficznymi. Problemem może być także skryptowanie. Z niektórymi rzeczami będziesz musiał po prostu ciężko umrzeć. Nie jestem do końca pewien, jaki byłby „poziom” cofnięcia (chociaż możesz zrobić mały pokój ze znakiem informującym, że wystąpił błąd, po prostu upewnij się, że zapisywanie jest wyłączone, ponieważ nie chcesz, aby gracz w nim utknął).

W przypadku „nowego”, przy tworzeniu gier lepiej może być skorzystanie z jakiejś fabryki. Może dać ci różne inne korzyści, takie jak wstępna alokacja przedmiotów, zanim rzeczywiście ich potrzebujesz i / lub ich masowe wykonanie. Ułatwia także tworzenie globalnego indeksu obiektów, których można używać do zarządzania pamięcią, znajdowania rzeczy według identyfikatora (chociaż można to zrobić również w konstruktorach). Oczywiście można tam również obsługiwać błędy alokacji.


2

Najważniejszą kwestią związaną z brakiem kontroli i awarii środowiska uruchomieniowego jest to, że haker może wykorzystać awarię do przejęcia procesu, a być może później maszyny.

Teraz haker nie może wykorzystać faktycznej awarii, gdy tylko nastąpi awaria, proces jest bezużyteczny i nieszkodliwy. Jeśli po prostu czytasz ze wskaźnika zerowego, to czysta awaria, popełniasz błąd, a proces umiera natychmiast.

Niebezpieczeństwo pojawia się, gdy wykonasz polecenie, które nie jest technicznie nielegalne, ale nie było to, co zamierzałeś zrobić, haker może być w stanie skierować to działanie przy użyciu starannie spreparowanych danych, które w przeciwnym razie powinny być bezpieczne.

Nieprzechwycony wyjątek nie jest tak naprawdę prawdziwą awarią, to tylko sposób na zakończenie programu. Wyjątki zdarzają się tylko dlatego, że ktoś napisał bibliotekę, która konkretnie zgłasza wyjątek w określonych okolicznościach. Zazwyczaj, aby uniknąć wpadnięcia w nieoczekiwaną sytuację, która może być otworem dla hakera.

W rzeczywistości niebezpiecznym posunięciem jest wychwytywanie wyjątków zamiast pozwalania im na poślizgnięcie, co nie oznacza, że ​​nigdy nie powinieneś tego robić, ale upewnij się, że łapiesz tylko tych, o których wiesz, że możesz sobie poradzić, a resztę wymknąć. W przeciwnym razie ryzykujesz cofnięciem środków bezpieczeństwa ostrożnie umieszczonych w bibliotece.

Skoncentruj się na błędach, które niekoniecznie rzucają wyjątki, takich jak pisanie poza końcem tablicy. Twój program nie byłby w stanie tego zrobić?


2

Musisz spojrzeć na swoje wyjątki i warunki, które mogą je wywołać. Cholernie dużo czasu, dużej części tego, do czego ludzie używają wyjątków, można uniknąć dzięki odpowiednim kontrolom podczas cyklu programowania / debugowania kompilacji / testowania. Używaj asercji, przekaż referencje, zawsze inicjuj lokalne zmienne przy deklaracji, memset-zero wszystkich alokacji, NULL po darmowym itp., A strasznie dużo teoretycznych wyjątków po prostu odchodzi.

Zastanów się: jeśli coś zawiedzie i może być kandydatem do zgłoszenia wyjątku - jaki jest wpływ? Ile to ma znaczenie? Jeśli nie powiedzie się alokacja pamięci w grze (na przykład w oryginalnym przykładzie), na ogół masz większy problem na ręce niż sposób obsługi przypadku awarii, a obsługa przypadku awarii za pomocą wyjątku nie spowoduje większego problemu Idź stąd. Gorzej - może faktycznie ukrywać fakt, że nawet większy problem istnieje. Chcesz tego? Wiem, że nie. Wiem, że jeśli nie uda mi się przydzielić pamięci, chcę rozbić się i spalić okropnie i zrobić to tak blisko punktu awarii, jak to możliwe, aby móc włamać się do debuggera, ustalić, co się dzieje, i naprawić to poprawnie.


1

Nie jestem pewien, czy cytowanie kogoś innego jest właściwe na SE, ale nie sądzę, aby żadna z pozostałych odpowiedzi tak naprawdę nie dotyczyła tego.

Cytując Jasona Gregory'ego z jego książki Game Engine Architecture . (ŚWIETNA KSIĄŻKA!)

„Obsługa strukturalnego wyjątku (SEH) powoduje znaczne obciążenie programu. Każda ramka stosu musi zostać powiększona, aby zawierała dodatkowe informacje wymagane przez proces odwijania stosu. Ponadto odwijanie stosu jest zwykle bardzo wolne - rzędu dwóch do trzech razy droższe od zwykłego powrotu z funkcji. Ponadto, jeśli choć jedna funkcja w twoim programie (lub bibliotece) używa SEH, cały program musi używać SEH. Kompilator nie może wiedzieć, które funkcje mogą znajdować się nad tobą na stosie wywołań, gdy zgłaszasz wyjątek. ”

Biorąc to pod uwagę, mój silnik, który buduję, nie używa wyjątków. Wynika to z wcześniej wspomnianego potencjalnego kosztu wydajności oraz z faktu, że do wszystkich moich celów kody powrotu błędów działają dobrze. Jedyne, czego naprawdę potrzebuję, aby znaleźć awarie, to inicjowanie i ładowanie zasobów, aw takich przypadkach zwracanie wartości logicznej wskazującej sukces działa dobrze.


2
Strukturalna obsługa wyjątków jest szczególnym rodzajem obsługi wyjątków dostępnym w systemie Windows, co nie jest tym samym, co generalnie wyjątki w C ++.
Kylotan

Doh, nie mogę uwierzyć, że o tym nie wiedziałem. Dziękuję za poprawienie mnie!
KlashnikovKid

0

Zawsze dbam o to, by mój kod był bezpieczny, po prostu dlatego, że łatwiej jest wiedzieć, gdzie pojawiają się problemy, ponieważ mogę rejestrować coś za każdym razem, gdy zostanie wygenerowany lub złapany wyjątek.

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.