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
Czy naprawdę piszesz kod bezpieczny wyjątek?
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
i std::thread
) będzie wyjątek, aby zamknąć bezpiecznie.
Czy na pewno Twój ostatni kod „gotowy do produkcji” jest wyjątkowo bezpieczny?
Czy możesz być nawet pewien, że tak jest?
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.
Czy znasz i / lub faktycznie korzystasz z alternatyw, które działają?
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:
- none : Twój kod nigdy nie powinien tego oferować. Ten kod przecieka wszystko i ulega awarii przy pierwszym zgłoszonym wyjątku.
- podstawowa : jest to gwarancja, którą musisz przynajmniej zaoferować, to znaczy, jeśli zostanie zgłoszony wyjątek, nie wyciekną żadne zasoby, a wszystkie obiekty będą nadal w całości
- strong : Przetwarzanie zakończy się powodzeniem lub wyrzuci wyjątek, ale jeśli zgłosi wyjątek, dane będą w tym samym stanie, jakby przetwarzanie w ogóle się nie rozpoczęło (daje to możliwość transakcyjną C ++)
- nothrow / nofail : Przetwarzanie zakończy się powodzeniem.
Przykład kodu
Poniższy kod wydaje się być poprawnym C ++, ale tak naprawdę oferuje gwarancję „brak”, a zatem nie jest poprawny:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
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:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
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:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
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 i X
nie wycieknie, ponieważ jest własnością inteligentnego wskaźnika.
Następnie tworzymy kopię t2
z t
, 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
i t2
. 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, to t
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”:
- [ja] Zamiana nigdy się nie zawiedzie (nawet nie pisz zamiany rzucania)
- [nobar] To dobra rekomendacja dla niestandardowych
swap()
funkcji. Należy jednak zauważyć, że std::swap()
może się nie powieść na podstawie operacji, których używa wewnętrznie
domyś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 dla vector
, deque
i list
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
t.integer += 1;
nie ma gwarancji, że przelew się nie wydarzy, NIE jest wyjątkowo bezpieczny, aw rzeczywistości może technicznie wywołać UB! (Przepełnienie ze znakiem to UB: C ++ 11 5/4 „Jeśli podczas oceny wyrażenia wynik nie jest zdefiniowany matematycznie lub nie mieści się w zakresie reprezentatywnych wartości dla jego typu, zachowanie jest niezdefiniowane.”) Należy zauważyć, że bez znaku liczby całkowite nie przepełniają się, lecz wykonują swoje obliczenia w klasie równoważności modulo 2 ^ # bitów.
Dionadar odnosi się do następującej linii, która rzeczywiście ma nieokreślone zachowanie.
t.integer += 1 ; // 1. nothrow/nofail
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.