Obsługa wyjątków (EH) wydaje się być obecnym standardem, a przeszukując sieć, nie mogę znaleźć żadnych nowych pomysłów ani metod, które starałyby się je ulepszyć lub zastąpić (no cóż, istnieją pewne odmiany, ale nic nowego).
Chociaż większość ludzi wydaje się go ignorować lub po prostu go akceptuje, EH ma pewne poważne wady: wyjątki są niewidoczne dla kodu i tworzy wiele, wiele możliwych punktów wyjścia. Joel on software napisał o tym artykuł . Porównanie goto
idealnie pasuje, sprawiło, że pomyślałem o EH.
Staram się unikać EH i po prostu używam wartości zwracanych, wywołań zwrotnych lub czegokolwiek, co pasuje do celu. Ale kiedy musisz napisać niezawodny kod, po prostu nie możesz teraz zignorować EH : zaczyna się od new
znaku, który może zgłosić wyjątek, zamiast zwracać 0 (jak za dawnych czasów). Powoduje to, że każda linia kodu C ++ jest podatna na wyjątek. A potem więcej miejsc w podstawowym kodzie C ++ generuje wyjątki ... std lib robi to i tak dalej.
To tak, jakby chodzić po chwiejnych terenach . Teraz jesteśmy zmuszeni dbać o wyjątki!
Ale to trudne, to naprawdę trudne. Musisz nauczyć się pisać wyjątkowy kod bezpieczny, a nawet jeśli masz z tym pewne doświadczenie, nadal będzie konieczne podwójne sprawdzenie dowolnego wiersza kodu, aby był bezpieczny! Lub zaczynasz wszędzie umieszczać bloki try / catch, co zaśmieca kod, aż osiągnie stan nieczytelności.
EH zastąpił stare czyste, deterministyczne podejście (zwracane wartości ..), które miało tylko kilka, ale zrozumiałych i łatwych do rozwiązania wad, podejściem, które tworzy wiele możliwych punktów wyjścia w kodzie, a jeśli zaczniesz pisać kod wychwytujący wyjątki (to, co w pewnym momencie są do tego zmuszone), a nawet tworzy wiele ścieżek w kodzie (kod w blokach catch, pomyśl o programie serwera, w którym potrzebujesz funkcji logowania innych niż std :: cerr ..). EH ma zalety, ale nie o to chodzi.
Moje aktualne pytania:
- Czy naprawdę piszesz kod bezpieczny wyjątek?
- Czy na pewno Twój ostatni kod „gotowy do produkcji” jest wyjątkowo bezpieczny?
- Czy możesz być nawet pewien, że tak jest?
- Czy znasz i / lub faktycznie korzystasz z alternatyw, które działają?
źródło
Odpowiedzi:
Twoje pytanie zawiera stwierdzenie, że „Pisanie kodu bezpiecznego dla wyjątków jest bardzo trudne”. Najpierw odpowiem na twoje pytania, a następnie odpowiem na ukryte pytanie za nimi.
Odpowiadanie na pytania
Oczywiście, że tak.
To jest powód, dla którego Java straciła wiele na atrakcyjności dla mnie jako programisty C ++ (brak semantyki RAII), ale dygresję: To jest pytanie C ++.
Jest to w rzeczywistości konieczne, gdy trzeba pracować z kodem STL lub Boost. Na przykład, nici C ++ (
boost::thread
istd::thread
) będzie wyjątek, aby zamknąć bezpiecznie.Pisanie kodu bezpiecznego dla wyjątków jest jak pisanie kodu wolnego od błędów.
Nie możesz być w 100% pewien, że Twój kod jest wyjątkowo bezpieczny. Ale potem dążysz do tego, używając dobrze znanych wzorów i unikając dobrze znanych anty-wzorów.
W C ++ nie ma realnych alternatyw (tzn. Musisz wrócić do C i unikać bibliotek C ++, a także zewnętrznych niespodzianek, takich jak Windows SEH).
Pisanie kodu bezpiecznego wyjątku
Do bezpiecznego kodu zapisu wyjątku, trzeba wiedzieć, pierwszy , jaki poziom bezpieczeństwa wyjątku każda instrukcja piszesz jest.
Na przykład a
new
może zgłosić wyjątek, ale przypisanie wbudowanego (np. Int lub wskaźnika) nie zakończy się niepowodzeniem. Zamiana nigdy się nie zawiedzie (nigdy nie pisz zamiany rzucania),std::list::push_back
może rzucić ...Gwarancja wyjątku
Pierwszą rzeczą do zrozumienia jest to, że musisz być w stanie ocenić gwarancję wyjątku oferowaną przez wszystkie swoje funkcje:
Przykład kodu
Poniższy kod wydaje się być poprawnym C ++, ale tak naprawdę oferuje gwarancję „brak”, a zatem nie jest poprawny:
Cały mój kod piszę z myślą o tego rodzaju analizach.
Najniższa oferowana gwarancja jest podstawowa, ale wtedy kolejność każdej instrukcji powoduje, że cała funkcja jest „brak”, ponieważ jeśli 3. wyrzuci, x wycieknie.
Pierwszą rzeczą do zrobienia byłoby uczynienie funkcji „podstawową”, czyli umieszczenie x w inteligentnym wskaźniku, dopóki nie będzie bezpiecznie własnością listy:
Teraz nasz kod oferuje „podstawową” gwarancję. Nic nie wycieknie, a wszystkie obiekty będą w prawidłowym stanie. Ale moglibyśmy zaoferować więcej, czyli silną gwarancję. To może być kosztowne i dlatego nie cały kod C ++ jest silny. Spróbujmy:
Zmieniliśmy kolejność operacji, najpierw tworząc i ustawiając
X
odpowiednią wartość. Jeśli jakakolwiek operacja zakończy się niepowodzeniem,t
nie jest modyfikowana, więc operację od 1 do 3 można uznać za „silną”: jeśli coś rzuci,t
nie zostanie zmodyfikowane iX
nie wycieknie, ponieważ jest własnością inteligentnego wskaźnika.Następnie tworzymy kopię
t2
zt
, a działanie tej kopii z eksploatacji 4 do 7. Jeśli coś rzuca,t2
jest modyfikowana, ale potem,t
wciąż jest oryginalny. Nadal oferujemy silną gwarancję.Następnie zamieniamy
t
it2
. Operacje wymiany nie powinny być wykonywane w C ++, więc miejmy nadzieję, że zamiana, dla której napisałeś,T
jest nothrow (jeśli nie jest, przepisz, żeby nie była).Tak więc, jeśli dojdziemy do końca funkcji, wszystko się powiedzie (nie ma potrzeby zwracania typu) i
t
ma swoją wyjątek. Jeśli się nie powiedzie, tot
nadal ma swoją oryginalną wartość.Teraz, oferowanie silnej gwarancji może być dość kosztowne, więc nie staraj się oferować silnej gwarancji dla całego swojego kodu, ale jeśli możesz to zrobić bez kosztów (a wbudowane C ++ i inna optymalizacja mogą sprawić, że cały kod będzie droższy) , a następnie zrób to. Użytkownik funkcji będzie ci za to wdzięczny.
Wniosek
Pisanie kodu bezpiecznego dla wyjątków wymaga pewnego nawyku. Musisz ocenić gwarancję oferowaną przez każdą instrukcję, której użyjesz, a następnie musisz ocenić gwarancję oferowaną przez listę instrukcji.
Oczywiście, kompilator C ++ nie utworzy kopii zapasowej gwarancji (w moim kodzie oferuję gwarancję jako tag @warning doxygen), co jest trochę smutne, ale nie powinno powstrzymywać cię przed próbą napisania kodu bezpiecznego dla wyjątków.
Normalna awaria vs. błąd
W jaki sposób programista może zagwarantować, że funkcja bezawaryjna zawsze się powiedzie? W końcu funkcja może mieć błąd.
To prawda. Gwarancje wyjątków powinny być oferowane przez kod wolny od błędów. Ale w każdym języku wywołanie funkcji zakłada, że funkcja jest wolna od błędów. Żaden rozsądny kod nie chroni się przed możliwością wystąpienia błędu. Napisz kod najlepiej, jak potrafisz, a następnie zaoferuj gwarancję, zakładając, że jest on wolny od błędów. A jeśli występuje błąd, popraw go.
Wyjątek stanowią wyjątkowe błędy przetwarzania, a nie błędy w kodzie.
Ostatnie słowa
Teraz pytanie brzmi: „Czy warto?”.
Oczywiście, że jest. Posiadanie funkcji „nothrow / no-fail” wiedząc, że funkcja się nie zawiedzie, jest wielkim dobrodziejstwem. To samo można powiedzieć o „silnej” funkcji, która umożliwia pisanie kodu z semantyką transakcyjną, taką jak bazy danych, z funkcjami zatwierdzania / wycofywania zmian, przy czym zatwierdzanie jest normalnym wykonaniem kodu, z wyjątkami będącymi wycofywaniem.
Zatem „podstawowa” jest najmniejszą gwarancją, którą powinieneś zaoferować. C ++ jest tam bardzo silnym językiem, którego zakresy pozwalają uniknąć wycieków zasobów (coś, co dla śmieciarza byłoby trudne do zaoferowania dla bazy danych, połączenia lub uchwytów plików).
Tak więc, o ile ja to widzę, to jest warto.
Edytuj 2010-01-29: O zamianie bez rzucania
nobar skomentował, co moim zdaniem, jest dość trafny, ponieważ jest częścią „jak napisać kod bezpieczny wyjątku”:
swap()
funkcji. Należy jednak zauważyć, żestd::swap()
może się nie powieść na podstawie operacji, których używa wewnętrzniedomyślnie
std::swap
będą tworzyć kopie i przypisania, które dla niektórych obiektów mogą rzucać. Tak więc domyślna zamiana mogłaby rzucać, używana dla twoich klas, a nawet dla klas STL. Jeśli chodzi o standard C ++, operacja zamiany dlavector
,deque
ilist
nie będzie rzucać, podczas gdy może to zrobić,map
jeśli funktor porównawczy może rzucić na budowę kopii (patrz The C ++ Programming Language, wydanie specjalne, dodatek E, E.4.3 . Zamień ).Patrząc na implementację wymiany wektora w Visual C ++ 2008, zamiana wektora nie będzie rzucać, jeśli dwa wektory mają ten sam alokator (tj. Normalny przypadek), ale wykona kopie, jeśli mają różne alokatory. Tak więc zakładam, że może to rzucić w tym ostatnim przypadku.
Tak więc oryginalny tekst nadal obowiązuje: nigdy nie pisz zamiany rzucania, ale komentarz nobara musi zostać zapamiętany: upewnij się, że wymieniane obiekty mają zamianę rzucania.
Edytuj 2011-11-06: Ciekawy artykuł
Dave Abrahams , który udzielił nam podstawowych / silnych / niezapowiedzianych gwarancji , opisał w swoim artykule swoje doświadczenie związane z zapewnieniem bezpieczeństwa wyjątku STL:
http://www.boost.org/community/exception_safety.html
Spójrz na 7 punkt (automatyczne testowanie w celu zapewnienia bezpieczeństwa wyjątkowego), gdzie polega on na automatycznych testach jednostkowych, aby upewnić się, że każdy przypadek jest testowany. Myślę, że ta część jest doskonałą odpowiedzią na pytanie autora „ Czy możesz być pewien, że tak jest? ”.
Edytuj 2013-05-31: Komentarz od dionadaru
Dionadar odnosi się do następującej linii, która rzeczywiście ma nieokreślone zachowanie.
Rozwiązaniem tutaj jest sprawdzenie, czy liczba całkowita ma już maksymalną wartość (używanie
std::numeric_limits<T>::max()
) przed dodaniem.Mój błąd przejdzie do sekcji „Normalna awaria vs. błąd”, czyli błąd. Nie unieważnia to rozumowania i nie oznacza, że kod bezpieczny dla wyjątków jest bezużyteczny, ponieważ niemożliwy do osiągnięcia. Nie można zabezpieczyć się przed wyłączeniem komputera, błędami kompilatora, a nawet błędami lub innymi błędami. Nie możesz osiągnąć doskonałości, ale możesz spróbować zbliżyć się jak najbliżej.
Poprawiłem kod, mając na uwadze komentarz Dionadara.
źródło
"finally" does exactly what you were trying to achieve
Oczywiście, że tak. Ponieważ tworzenie kodu łamliwego jest łatwe, w Javie 7 ostatecznie wprowadzonofinally
pojęcie „spróbuj z zasobem” (10 lat po C # i 3 dekady po niszczarkach C ++). Właśnie tę kruchość krytykuję. Jeśli chodzi o : W tym przypadku przemysł nie zgadza się z twoim gustem, ponieważ języki Garbage Collected mają tendencję do dodawania instrukcji inspirowanych RAII (C # i Java ).using
Just because [finally] doesn't match your taste (RAII doesn't match mine, [...]) doesn't mean it's "failing"
using
try
RAII doesn't match mine, since it needs a new struct every single darn time, which is tedious sometimes
Nie, nie ma. Możesz użyć inteligentnych wskaźników lub klas narzędzi do „ochrony” zasobów.Pisanie kodu bezpiecznego dla wyjątków w C ++ nie polega na użyciu wielu bloków try {} catch {}. Chodzi o dokumentowanie, jakie rodzaje gwarancji zapewnia Twój kod.
Polecam przeczytanie serii Guru tygodnia Tygodnia Herb Suttera , w szczególności części 59, 60 i 61.
Podsumowując, istnieją trzy poziomy bezpieczeństwa wyjątkowego, które możesz zapewnić:
Osobiście odkryłem te artykuły dość późno, więc większość mojego kodu C ++ zdecydowanie nie jest bezpieczna dla wyjątków.
źródło
Niektórzy z nas korzystają z wyjątku od ponad 20 lat. PL / I je ma na przykład. Przesłanka, że są one nową i niebezpieczną technologią, wydaje mi się wątpliwa.
źródło
Po pierwsze (jak stwierdził Neil), SEH to Structured Exception Handling Microsoftu. Jest podobny do przetwarzania wyjątków w C ++, ale nie identyczny. W rzeczywistości musisz włączyć obsługę wyjątków C ++, jeśli chcesz w Visual Studio - domyślne zachowanie nie gwarantuje zniszczenia obiektów lokalnych we wszystkich przypadkach! W obu przypadkach obsługa wyjątków nie jest trudniejsza , po prostu inna .
Teraz twoje aktualne pytania.
Tak. We wszystkich przypadkach staram się o wyjątkowy kod bezpieczny. Ewangelizuję za pomocą technik RAII w celu uzyskania dostępu do zasobów w określonym zakresie (np.
boost::shared_ptr
Do pamięci,boost::lock_guard
do blokowania). Ogólnie rzecz biorąc, konsekwentne stosowanie technik RAII i technik ochrony zakresu znacznie ułatwi pisanie kodu bezpiecznego wyjątku. Sztuką jest dowiedzieć się, co istnieje i jak go zastosować.Nie. Jest tak bezpieczny, jak to jest. Mogę powiedzieć, że nie widziałem błędu procesu z powodu wyjątku przez kilka lat działalności 24/7. Nie oczekuję idealnego kodu, tylko dobrze napisany kod. Oprócz zapewnienia wyjątkowego bezpieczeństwa, powyższe techniki gwarantują poprawność w sposób, który jest prawie niemożliwy do osiągnięcia przy pomocy
try
/catch
bloków. Jeśli łapiesz wszystko w swoim najwyższym zakresie kontrolnym (wątek, proces itp.), Możesz być pewien, że będziesz nadal działał w obliczu wyjątków (przez większość czasu ). Te same techniki pomogą ci nadal działać poprawnie w obliczu wyjątków beztry
/catch
bloków wszędzie .Tak. Możesz być pewien dokładnego audytu kodu, ale tak naprawdę nikt tego nie robi? Regularne przeglądy kodu i ostrożni programiści bardzo się do tego przyczyniają.
Przez lata próbowałem kilku odmian, takich jak kodowanie stanów w wyższych bitach (ala
HRESULT
s ) lub ten okropnysetjmp() ... longjmp()
hack. Oba z nich rozpadają się w praktyce, choć na zupełnie inne sposoby.W końcu, jeśli nauczysz się stosować kilka technik i dokładnie zastanawiasz się, gdzie możesz rzeczywiście zrobić coś w odpowiedzi na wyjątek, otrzymasz bardzo czytelny kod, który jest wyjątkowo bezpieczny. Możesz to podsumować, przestrzegając następujących zasad:
try
/catch
kiedy możesz coś zrobić z konkretnym wyjątkiemnew
lubdelete
kodustd::sprintf
,snprintf
i ogólnie tablic - używajstd::ostringstream
do formatowania i zamieniaj tablice nastd::vector
istd::string
Mogę tylko zalecić, abyś nauczył się prawidłowo używać wyjątków i zapomniał o kodach wyników, jeśli planujesz pisać w C ++. Jeśli chcesz uniknąć wyjątków, możesz rozważyć pisanie w innym języku, który ich nie ma lub zapewnia im bezpieczeństwo . Jeśli chcesz naprawdę nauczyć się, jak w pełni korzystać z C ++, przeczytaj kilka książek z Herb Sutter , Nicolai Josuttis i Scott Meyers .
źródło
new
lubdelete
kodu”: domyślnie rozumiem, że masz na myśli konstruktor lub destruktor.delete
nigdy nie należy go używać poza implementacjątr1::shared_ptr
i tym podobnych.new
może być używany pod warunkiem, że jego użycie jest podobnetr1::shared_ptr<X> ptr(new X(arg, arg));
. Ważną częścią jest to, że wyniknew
przechodzi bezpośrednio do zarządzanego wskaźnika. Strona naboost::shared_ptr
Dobrych Praktyk opisuje najlepsze.Nie można napisać kodu bezpiecznego dla wyjątków przy założeniu, że „dowolna linia może wyrzucić”. Projektowanie kodu bezpiecznego dla wyjątków opiera się krytycznie na pewnych umowach / gwarancjach, których należy się spodziewać, przestrzegać, przestrzegać i wdrażać w kodzie. Konieczne jest posiadanie kodu, który gwarantuje, że nigdy nie wyrzuci. Istnieją inne rodzaje gwarancji wyjątkowych.
Innymi słowy, tworzenie kodu bezpiecznego dla wyjątków jest w dużej mierze kwestią projektu programu, a nie tylko zwykłego kodowania .
źródło
Z pewnością zamierzam.
Jestem pewien, że moje serwery 24/7 zbudowane przy użyciu wyjątków działają 24/7 i nie przeciekają pamięci.
Bardzo trudno jest upewnić się, że dowolny kod jest poprawny. Zazwyczaj można przejść tylko według wyników
Nie. Używanie wyjątków jest czystsze i łatwiejsze niż jakakolwiek alternatywa, z której korzystałem przez ostatnie 30 lat w programowaniu.
źródło
Pomijając pomieszanie wyjątków SEH i C ++, musisz mieć świadomość, że wyjątki mogą być zgłaszane w dowolnym momencie, i napisz o tym swój kod. Potrzeba bezpieczeństwa wyjątkowego jest w dużej mierze tym, co napędza użycie RAII, inteligentnych wskaźników i innych nowoczesnych technik C ++.
Jeśli postępujesz zgodnie z ustalonymi wzorcami, pisanie kodu bezpiecznego dla wyjątków nie jest szczególnie trudne, a w rzeczywistości jest łatwiejsze niż pisanie kodu, który poprawnie obsługuje zwroty błędów we wszystkich przypadkach.
źródło
EH jest ogólnie dobre. Ale implementacja C ++ nie jest zbyt przyjazna, ponieważ naprawdę trudno jest stwierdzić, jak dobry jest twój zasięg wychwytywania wyjątków. Java na przykład ułatwia to, kompilator może zawieść, jeśli nie obsłużysz możliwych wyjątków.
źródło
noexcept
.Naprawdę lubię pracować z Eclipse i Javą (nowość w Javie), ponieważ generuje błędy w edytorze, jeśli brakuje Ci programu obsługi EH. To sprawia, że dużo trudniej jest zapomnieć o obsłudze wyjątku ...
Dodatkowo dzięki narzędziom IDE automatycznie dodaje blok try / catch lub inny blok catch.
źródło
Niektórzy z nas preferują języki takie jak Java, które zmuszają nas do deklarowania wszystkich wyjątków zgłaszanych przez metody, zamiast uczynić je niewidocznymi, jak w C ++ i C #.
Prawidłowo wykonane wyjątki przewyższają kody powrotu błędów, jeśli z innego powodu nie trzeba ręcznie propagować awarii w łańcuchu połączeń.
To powiedziawszy, programowanie biblioteki API niskiego poziomu powinno prawdopodobnie unikać obsługi wyjątków i trzymać się kodów powrotu błędów.
Z mojego doświadczenia wynika, że trudno jest napisać czysty kod obsługi wyjątków w C ++. W końcu
new(nothrow)
dużo używam .źródło
new(std::nothrow)
nie wystarczy. Nawiasem mówiąc, łatwiej jest napisać kod bezpieczny dla wyjątków w C ++ niż w Javie: en.wikipedia.org/wiki/Resource_Acquisition_Is_InitializationDokładam wszelkich starań, aby napisać kod bezpieczny dla wyjątków, tak.
Oznacza to, że zwracam uwagę na to, które linie mogą rzucać. Nie każdy może i należy mieć to na uwadze. Kluczem do sukcesu jest przemyślenie i zaprojektowanie kodu tak, aby spełniał gwarancje wyjątków określone w standardzie.
Czy można napisać tę operację, aby zapewnić silną gwarancję wyjątku? Czy muszę zadowolić się podstawowym? Które linie mogą zgłaszać wyjątki i jak mogę się upewnić, że jeśli to zrobią, nie uszkodzą obiektu?
źródło
Czy naprawdę piszesz kod bezpieczny wyjątek? [Nie ma takiej rzeczy. Wyjątki stanowią papierową tarczę przed błędami, chyba że masz środowisko zarządzane. Dotyczy to pierwszych trzech pytań.]
Czy znasz i / lub faktycznie korzystasz z alternatyw, które działają? [Alternatywa do czego? Problem polega na tym, że ludzie nie oddzielają rzeczywistych błędów od normalnego działania programu. Jeśli jest to normalne działanie programu (tzn. Nie znaleziono pliku), to tak naprawdę nie jest to obsługa błędów. Jeśli jest to rzeczywisty błąd, nie ma sposobu, aby go „obsłużyć” lub nie jest to rzeczywisty błąd. Twoim celem jest dowiedzieć się, co poszło nie tak, albo zatrzymać arkusz kalkulacyjny i zapisać błąd, ponownie uruchomić sterownik do tostera lub po prostu modlić się, aby myśliwiec nadal mógł latać, nawet jeśli jego oprogramowanie jest wadliwe i ma nadzieję na najlepsze.]
źródło
Dużo (powiedziałbym nawet większość) ludzi.
Najważniejsze w wyjątkach jest to, że jeśli nie napiszesz żadnego kodu obsługi - wynik jest całkowicie bezpieczny i dobrze się zachowuje. Zbyt chętny do paniki, ale bezpieczny.
Musisz aktywnie popełniać błędy w modułach obsługi, aby uzyskać coś niebezpiecznego, a tylko catch (...) {} będzie porównywany do ignorowania kodu błędu.
źródło
f = new foo(); f->doSomething(); delete f;
jeśli metoda doSomething zgłasza wyjątek, oznacza to przeciek pamięci.