Wyjątki
... doświadczenie związane z jej realizacją [obsługi sytuacji wyjątkowych] w języku C++ nie było wielkie (...). To było niepokojące, choć wokół panowało zgodne przekonanie, że jakaś odpowiednia postać obsługi sytuacji wyjątkowych w języku C++ to dobry pomysł (...). Z tej ogólnej zasady wyraźnie wyłamywał się Doug McIlroy, który stwierdził, że możliwość korzystania z obsługi sytuacji wyjątkowych spowoduje, iż system będzie mniej niezawodny, ponieważ twórcy bibliotek i inni programiści, zamiast starać się zrozumieć i rozwiązywać problemy, będą zgłaszać sytuacje wyjątkowe. Tylko czas pokaże, czy przypuszczenie Douga było słuszne." Bjarne Stroustrup, "Projektowanie i rozwój języka C++"
Wiadomości podstawowe
Hmm... czas pokaże, ale mam nadzieję również, że czytelni(k|czka) sam oceni, czy takie obawy są uzasadnione. Wyjątki są mechanizmem pozwalającym obsłużyć różne sytuacje wyjątkowe, tzn. takie, które powinny się zdarzać relatywnie rzadko. Zazwyczaj jest to obsługa błędu, choć nie oznacza to wcale, że to miałby być jakiś wyjątkowy błąd - może być to błąd zupełnie spodziewany i oczekiwany, niemniej jego obsługa wymagałaby różnych przeskoków i kombinacji (jeśli ktoś się zapoznał z setjmp z rozdziału o bibliotekach C, to powinien się zorientować, o co chodzi, ale poznanie tych funkcji nie jest konieczne do poznania wyjątków).
Wyjątki służą głównie do tego, żeby zuniwersalizować system obsługi sytuacji wyjątkowych (no i przede wszystkim wyeliminować używanie setjmp/longjmp).
Problemy z obsługą sytuacji wyjątkowych
Spróbujmy rozważyć najpierw kilka standardowych przypadków wystąpienia w programie sytuacji wyjątkowej. Jedna z najbardziej denerwujących to nieprzydzielenie pamięci.
Block* b = malloc( sizeof (Block) ); if ( b == NULL ) // to co?
Jeśli coś takiego jak przydział pamięci odbywa się często (a zazwyczaj tak jest), to konia z rzędem temu, komu starczy cierpliwości na obsługę każdego z nieudanych przydziałów pamięci. A poza tym... załóżmy, że złapaliśmy taką sytuację, że nie przydzielono pamięci. I co wtedy? Załóżmy, że jesteśmy w środku programu, zrobilismy mnóstwo obiektów, wszystko ze sobą współpracuje, chcieliśmy odrobinę dodatkowej pamięci na wykonanie jakiejś małej operacji a tu - CIACH! Nie ma pamięci... Stwierdziliśmy fakt i... co właściwie możemy zrobić? Zgłosić buga? A jeśli bez tego obiektu program nie będzie się kwalifikował do dalszego wykonywania? A jeśli użytkownik z powodu wyłożenia się programu straci ważne dane?
Ja osobiście uważam (i prawdopodobnie twórcy C++ zgadzają się ze mną), że błąd przydziału pamięci jest takiej samej rangi błędem, co błędne odwołanie się do pamięci. Dlatego do jego obsługi osobiście na UNIXie proponowałbym sygnały, aczkolwiek zazwyczaj trudno jest powiedzieć, co właściwie można zrobić kiedy nie ma pamięci (często to w praktyce oznacza, że nie ma nawet z czego wykroić kawałka bufora na komunikat o błędzie). Zresztą, brak pamięci jest czasem zawiniony przez program (bo żre bez umiaru), a czasem nie (ktoś inny żre bez umiaru, ale to na niego akurat padło). Jest to zatem tzw. runtime-error, z którego czasem można zrobić jakieś "recovery", a czasem nie.
Ale to jeszcze nic. W tamtym przypadku najczęściej robi się printf("Error: memory exhaused\n"); abort(); i koniec (doprowadzając użytkownika - w zależności od jego charakteru - do zdumienia lub furii). Załóżmy jednak (ogólno-teoretycznie), że podczas pozyskiwania zasobów, w którym każdy zasób przydzielony w następnej kolejności korzysta z zasobów uzyskanych wcześniej (w uproszczeniu przyjmijmy, że ze wszystkich). I nagle jednego z zasobów nie da się uzyskać. I co? Zwolnić wszystkie zasoby? A co z zasobami, które nie zostały przydzielone? Kod, który uwzględnia wszystkie "wcześniej przydzielone" zasoby wygląda mniej więcej tak:
if ( !Pozyskaj( 1 ) ) exit( 1 ); if ( !Pozyskaj( 2 ) ) { Zwolnij( 1 ); exit( 1 ); } if ( !Pozyskaj( 3 ) ) { Zwolnij( 2 ); Zwolnij( 1 ); exit( 1 ); }
i tak dalej; komentarz chyba jest zbędny... Zresztą to jest właśnie jeden z dowodów bzdurności twierdzenia, jakoby programy w C mogły być bardzo szybkie - jasne, pewnie programiści myślą, że takie sprawdzanie warunków na okrągło nic nie kosztuje.
Jest jeszcze inna rzecz. Wyobraźmy sobie funkcję, która ma zwrócić jakąś wartość, a jej wywołanie może się również nie powieść, ale każda zwracana wartość jest prawidłowa (jak choćby różne matematyczne funkcje z ograniczoną dziedziną np. log, czy sqrt). Matematyczne funkcje pod systemami UNIXowymi często kończą się wywołaniem sygnału, ale użytkownik nie za bardzo może sobie na to pozwolić. Jedyna możliwość w takim wypadku to zwrócić uniwersalną wartość (jak np. często stosuje się zwrócenie maksymalnej wartości danego zakresu liczbowego w przypadku dzielenia przez zero), ale nie ma wtedy możliwości powiadomienia o błędzie.
Ale to jest wszystko jeszcze nic. Wyobraźmy sobie, że wywołanie funkcji przebiega wedle takiej sekwencji:
Runaway –> Jump –> Caught –> Flash
I załóżmy, że w funkcji Flash wystąpiła sytuacja uniemożliwiająca dalsze wykonywanie funkcji Runaway! Klasyczne rozwiązanie polega na tym, że Flash zwraca niepowodzenie, na co Caught zwraca niepowodzenie, na co Jump zwraca niepowodzenie... a to jest i tak jeszcze jedna z mniej skomplikowanych sekwencji!
Teoria wyjątków
Wyjątek jest to po prostu obiekt i wcale nie musi być jakiegoś ściśle określonego typu. Jednak shierarchizowanie klas, które będą służyć do rzucania wyjątkami jest jak najbardziej pożądane (i oczywiście takowe istnieją, proszę sobie obejrzeć pliki nagłówkowe stdexcept i exception). Zaczyna się od tego, że pewien fragment kodu PRÓBUJE się (ang. try) wykonać. Tam, w razie wystąpienia sytuacji wyjątkowej, RZUCA się (ang. throw) wyjątek, który następnie powinien zostać PRZECHWYCONY (ang. catch). Wygląda to w następujący sposób:
try { // tu kod wrażliwy na wyjątek } catch ( <k1> ) { // tu obsługa przechwycenia wyjątku klasy <k1> } catch ( <k2> ) { // tu obsługa przechwycenia wyjątku klasy <k2> } catch ( ... ) { // tu obsługa jakiegokolwiek wyjątku }
Oczywiście wyrażenie `catch(...)' musi wystąpić dopiero na końcu listy, gdyż ta klauzula obsługuje cokolwiek jej w ręce wpadnie (choć od razu wypadnie: nie ma możliwości żadnego dostępu do obiektu wyjątku w takiej klauzuli). Co prawda jako argument klauzuli catch wpisałem sam typ, aczkolwiek jest to normalny argument, taki sam, jak argument funkcji. Klauzula catch jest tutaj jakby przeciążoną funkcją, gdyż obsługa jest wybierana na podstawie klasy wyjątku. Jednak nie działają tutaj żadne reguły rozstrzygania niejednoznaczności; tutaj panuje zasada "kto pierwszy ten lepszy". Zatem z reguły najpierw należy umieszczać klasy jak najdalej pochodne, później zaś dopiero klasy bardziej pierwotne.
Rzucanie wyjątkiem
Wyjątkiem rzuca się przy pomocy instrukcji throw. Najczęściej wyjątek tworzy się jako obiekt tymczasowy, bo tak jest najwygodniej. Mamy zresztą do dyspozycji mnóstwo wyjątków standardowych (we wspomnianych plikach nagłówkowych). Zatem możemy sobie od niechcenia rzucić wyjątkiem (zawsze to lepiej, niż mięsem, jednak skutki są o wiele bardziej tragiczne :*):
throw exception(); // exception() to konstruktor; tworzy obiekt tymczasowy
Instrukcja throw jako argumentu potrzebuje obiektu; w tym wypadku jest to obiekt tymczasowy. Posłuży on do przeniesienia informacji do odpowiedniej dla niego klauzuli catch (jeśli taka zostanie znaleziona, bo jeśli nie, to będzie bardzo źle...) i w końcu o to przecież nam chodzi. Jest to jedyna słuszna klasa dla obiektów wyjątków. Co oznacza w praktyce, że na upartego można stosować inną, ale jest to tylko utrudnianie sobie życia. Obiekt wyjątku zostanie skopiowany na rzecz przekazania go do klauzuli catch (nie znaczy to wcale, że musi zostać wywołany konstruktor kopiujący – gcc np. niczego nie kopiuje, tylko przekazuje oryginalny obiekt, w ramach dozwolonej optymalizacji – ale że klasa wyjątku musi mieć zdefiniowany konstruktor kopiujący).
Propagacja wyjątku
Rzucony w ten sposób wyjątek – co jest dla programisty chyba najważniejsze – zwija stos bieżącego zasięgu (ang. unwinds a stack - tu mnie ciągle niektórzy poprawiają, że "unwind" znaczy "rozwijać", ale ja wiem swoje - określenie "zwijanie" stosu oznacza, że niszczy się wszystkie zmienne lokalne, włącznie z wywołaniem ich destruktorów, podobnie jak się zwija - a nie rozwija - obóz po zakończeniu działań) i próbuje skoczyć do klauzuli catch najbliższej, która go obsłuży. Jeśli takiej nie ma w danej funkcji, wyjątek się nie ceregieli i zwija stos bieżącej funkcji po czym wykona skok do odpowiedniej dla siebie klauzuli catch w funkcji, z której ta bieżąca została wywołana. W efekcie, rzucony nie obsłużony wyjątek przelatuje po kolei wszystkim funkcjom przed oczami, przy okazji zwijając je, aż natrafi na taką, która go obsłuży (ten właśnie sposób postępowania wyjątków nazywa się semantyką zakończenia). Stos funkcji możnaby sobie wyobrazić jak drzewo, którego korzeń (ang. root!) to funkcja main. Zatem rzucony wyjątek likwiduje wszelkie funkcje wraz z ich zawartością (ale likwiduje uczciwie, w odróżnieniu od takiej np. funkcji exit(), tzn. niszczonym obiektom uczciwie wywołuje destruktory!) i tak jedzie z tym w dół (jak czołg) aż dotrze do funkcji, która go obsłuży. Jeśli dotrze do funkcji main, a ta również nie zechce go obsłużyć, nastąpi sytuacja zwana "niewyłapanym wyjątkiem" (ang. uncaught exception).
Akcja podejmowana w sytuacji "niewyłapanego wyjątku" polega na natychmiastowym wywołaniu funkcji terminate(). Zwracam tu jednak uwagę na dość istotną rzecz, mianowicie wywołanie terminate() może oznaczać, że nie wszystkie destruktory zostaną wywołane. Jest kwestią zależną od implementacji, czy w ogóle jakiekolwiek destruktory zostaną wywołane w sytuacji "niewyłapanego wyjątku". Implementacja może bowiem już w momencie rzucania wyjątku stwierdzić, że nie zostanie on przechwycony i od razu wywołać terminate() (zauważ, zatem, że w takiej sytuacji ominięte mogą być nie tylko destruktory obiektów globalnych, ale w ogóle jakichkolwiek, które powinny zostać wywołane).
Funkcja terminate() powoduje nienormalne zakończenie programu, tzn. polega na wywołaniu abort(). Użytkownik może częściowo wpłynąć na jej działanie, tzn. może dodać swoją własną funkcję, która zostanie wywołana przed abort(). Może również zakończyć działanie (np. przez exit()) zanim zostanie wywołana abort(). Obsługę tą ustawia się za pomocą funkcji set_terminate(), której deklaracja znajduje się w nagłówku <exception>.
Tu mała uwaga - pisałem wcześniej, że funkcji abort i exit nie należy w C++ używać. To jest właśnie jeden wyjątek. Jak najbardziej NALEŻY tych funkcji używać w funkcjach rejestrowanych jako handler terminate. Może i nie jest to specjalnie bezpieczne, ale w tym wypadku już i tak nie można nic na to poradzić. Takoż funkcja exit() jest w takiej sytuacji ABSOLUTNIE JEDYNĄ możliwością zwrócenia kodu powrotnego programu, gdyż z funkcji terminate() nie ma powrotu i trzeba się też liczyć z tym, że być może żadne obiekty z wyjątkiem zmiennych lokalnych handlera terminate() już dawno nie istnieją. Zwracam też uwagę, że nieprzechwycenie wyjątku jest jak najbardziej błędem w programie na etapie projektowania; program powinien być tak napisany, żeby sytuacja "niewyłapania wyjątku" nigdy nie mogła wystąpić. Wywołanie terminate() jest czymś jeszcze bardziej wyjątkowym, niż same wyjątki.
Przechwytywanie wyjątku
Dla wyjątku jednak lepiej jest, jeśli zostanie przechwycony. Zatem po dotarciu wyjątku do odpowiedniej klauzuli catch, wykonuje się procedura jego obsługi. Kiedy ta procedura się zakończy, wykonuje się kod znajdujący się za obszarem try/catch dalej jak gdyby nigdy nic.
Oczywiście może się zdarzyć, że wyjątek nie pasuje do żadnej klasy, występującej w klauzulach catch (przy czym catch(...) nie jest obowiązkowy), wtedy wszystko przebiega tak, jakby w tym miejscu żadnego przechwytywania nie było.
Podobnie, jak wyjątek doradzam przekazywać jako obiekt tymczasowy (choć można jak się chce), tak przechwytywać dany wyjątek również doradzam przez referencję (NIE przez wartość!). Powód jest prosty - wewnątrz klauzuli operujemy w ten sposób bezpośrednio na przekazanym obiekcie, a nie na jego kopii, co może mieć często dość istotne znaczenie (ogólnie w C++ należy unikać przekazywania obiektów przez wartość ze względu na czaso- i stosochłonność; poza tym dobrze jest rzucać wyjątkiem klasy polimorficznej, dzięki czemu można zbadać jego rzeczywisty typ; łapiąc wyjątek przez wartość pozbawiamy się tej możliwości). Zwracam oczywiście uwagę, że wyjątek MOŻE być kopiowany, więc dotyczy to tylko takich sytuacji, kiedy nie jest kopiowany.
Oczywiście nie musimy w danej serii klauzuli catch obsługiwać wszystkich wyjątków. Nawet możemy niektóre obsłużyć tylko częściowo. Można np. w danej klauzuli catch, dorzucić dodatkowe dane identyfikacyjne do danego wyjątku, po czym "puścić go dalej":
int Meele() { try { ... } catch ( XKling& x ) { string s = "Meele:"; s += x.what(); x.Setwhat( s ); throw; } }
Składnia `throw;' oznacza, że należy rzucić ten sam wyjątek, który się przechwyciło. Łatwo się domyślić zatem, że brak wśród klauzul catch wyrażenia catch(...) oznacza to samo, jakby jego obsługa brzmiała {throw;}.
Ograniczanie wyjątków
Przydatną niekiedy rzeczą (choć osobiście nie udało mi się jeszcze tego sprawdzić) jest ograniczanie klas, do jakich może należeć wyjątek, jaki funkcja ma "wypuszczać" (deklaracja taka bowiem dotyczy funkcji), czyli tzw. filtr na wyjątki. Jeśli chcemy, żeby dana funkcja obsługiwała np. tylko invalid_argument i out_of_range, deklarujemy ją w następujący sposób:
int Fn( int ) throw ( invalid_argument, out_of_range );
Funkcja o takiej deklaracji może rzucać na zewnątrz wyjątki tylko tych dwóch zadeklarowanych klas. W wypadku rzucenia wyjątku innej klasy (czyli w tym wypadku "nieoczekiwanego", ang. unexpected), wywoływana jest funkcja unexpected(), której domyślną akcją jest wywołanie terminate(). Podobnie jak terminate(), unexpected() może również zostać ustawiona przez set_unexpected(). Lista wyjątków może też być oczywiście pusta. Jak należy domniemać, przed wykonaniem unexpected() wykonuje się wszystko to, co i normalnie poprzedza wywołanie terminate()!
Zaznaczam jednak uczciwie, że ograniczanie wyjątków, jest jednym z "samobójczych" narzędzi. Takim samym "samobójczym" narzędziem jest oczywiście terminate. Jednak o ile terminate jest standardową reakcją na błąd programisty, o tyle unexpected jest jeszcze dodatkowo przez programistę programowalną. Jest to bardzo "Smalltalkowe" rozwiązanie, gdyż stanowi, że błąd programistyczny jest wykrywany przez wykonywany program (a nie przez kompilator). Tu wystarczy, że jakaś funkcja wywołana spod ograniczonej-na-wyjątki funkcji rzuci jednym z niewymienionych wyjątków i program idzie w buraki. Kompilator jest w stanie zareagować wcześniej tylko pod warunkiem, że zna kod wszystkich funkcji, jakie są spod tej ograniczonej-na-wyjątki wywoływane. Nawet zaś gdyby potrafił to program wiążący, to też nie wyłapie wszystkich tego typu błędów; np. nie dowie się tego przy wywoływaniu funkcji przez wskaźnik, czyli de facto również wywołania metod wirtualnych, a wskaźnik do funkcji z kolei można pobrać np. przez dlsym (temu jednak z kolei można zaradziż, ustanawiając ograniczanie na wyjątki dla wskaźnika do funkcji; tylko niektore kompilatory maja problem z implementacja tego "featuresa"). Inna byłaby sprawa, gdyby takie ograniczanie wyjątków nakładać na KAŻDĄ bez wyjątku funkcję. Ale komu chciałoby się w to bawić...
Zwalnianie zasobów
Pewnie wielu mi zarzuci, że przedstawiłem problem (z tym pozyskiwaniem zasobów), przedstawiłem narzędzia, ale nie przedstawiłem, jak ten problem rozwiązać - z przedstawionych informacji żadne sensowne rozwiązanie nie wynika. Ale ja mówiłem o nim na samym początku - wyjątek zwija stos i wywołuje DESTRUKTORY zwijanych obiektów. Zatem cały "power" wyjątków wcale nie leży w samych wyjątkach, ale w destruktorach. Mało kto jednak tą kwestię dobrze rozumie. Z tego właśnie powodu w wielu językach, które zerżnęły wyjątki z C++ (np. Java), a nawet w niektórych dialektach C++ (zwykle Microsofta, czy Borlanda) dodaje się jeszcze do serii catch taką klauzulę w stylu `finally'. Wygląda to mniej więcej tak:
try { // probably throw } catch ( e ) { // repair situation, probably exit } finally { // do the final things }
Klauzula `finally' ma zawrzeć to, co jest konieczne do wykonania czynności końcowych (zanim nastąpi wyjście z funkcji, które jest zawarte w klauzuli obsługi wyjątku `e'). W C++ niczego takiego nie ma. Czy to jest błąd twórców C++? Nie, przeciwnie, to `finally' jest idiotyzmem. Tzn. w języku takim jak Java jest to po prostu konieczna łata na dziurę, jakich w Javie jest pełno (tu jest to łata na brak destruktora; podobnie interfejs w Javie jest łatą na brak wielorakiego dziedziczenia).
Przedstawię tutaj tylko dwie sytuacje, ale chyba dość typowe.
int Fn() { try { ifstream in( "plik" ); FreadX( in ); // może rzucić wyjątkiem FreadY( in ); // też może rzucić wyjątkiem ofstream out( "plik.1" ); FwriteX( out ); // też może rzucić wyjątkiem FwriteY( out ); // też może rzucić wyjątkiem } catch ( exception& e ) { cout << "Error: " << e.what() << endl; } return 0; }
Załóżmy, że FwriteY rzuci wyjątkiem. Co wtedy? Klauzula `catch' nie może zawierać w sobie zamknięcia pliku `out', bo gdyby wyjątek wystąpił np. w FreadX to w ogóle tu nie ma czego zamykać. Tutaj jednak wszystko jest w porządku, bowiem destruktory ifstream i ofstream normalnie te pliki pozamykają (tylko te oczywiście, które w ogóle były otwierane) i to niezależnie od tego, czy funkcja skończy się normalnie, czy przez wyjątek. Ale co zrobić, jeśli mamy obiekty dynamiczne, które przecież trzeba zwalniać ręcznie? Na to też jest rada:
int Fn() { try { ifstream in( "dane" ); auto_ptr<Klocek> pk( new Klocek() ); pk->x = FreadX( in ); pk->y = FreadY( in ); // Ok, minęliśmy krytyczną sekcję DodajDoBazy( pk ); pk.release(); } catch ( exception& e ) { cout << "Error: " << e.what() << endl; } }
Jest tutaj zastosowana pewna sztuczka ze standardowym typem wzorcowym (o wzorcach później) auto_ptr. Typ ten opakowuje wskaźnik (do podanego typu), zatem do funkcji DodajDoBazy przekazywany jest obiekt jako `Klocek*'. Funkcje FreadX i FreadY mogą rzucić wyjątkiem. Jeśli się to stanie, zanim zostanie wywołane `pk.release()', obiekt, na który wskazuje pk zostanie zwolniony przez destruktor wskaźnika pk. Właśnie od tego jest auto_ptr, żeby tymczasowo obiektowi dynamicznemu nadał właściwości zmiennej lokalnej. Dopiero kiedy minie się sekcję krytyczną, auto_ptr zrzeka się kontroli nad obiektem przez wywołanie metody release(). Tu ważna uwaga! Zgodnie z obowiązującym standardem, metoda release() ZERUJE jednocześnie wskaźnik pk, należy go więc gdzieś przechować ZANIM się tą metodę wywoła.
Przy okazji - auto_ptr stanowi też dobre opakowanie dla wszelkich typów C tworzących głównie obiekty dynamiczne, np. typu FILE. Wzorce (które niedługo poznamy) mają to do siebie, że dowolne ich części można zmieniać jak się chce. Zatem dla auto_ptr<FILE> można sobie zdefiniować własny destruktor, który wykona sobie fclose - to jest najprostsze rozwiązanie. Można też oczywiście zdefiniować własną strukturę opakowującą. Jak widać więc, w C++ trzeba tylko dobrze umieć wykorzystać destruktory, a żadne `finally' nie jest tu potrzebne.
Tak dodatkowo wspomnę jeszcze o jednej ważnej rzeczy związanej z destruktorami. Wyobraźmy sobie taką sytuację, że rzucono wyjątkiem, zatem następuje zwijanie stosu. Ze zwijaniem stosu związane jest oczywiście wywołanie destruktora. Co by się jednak stało, gdyby wywołany w ten sposób destruktor sam rzucił wyjątkiem?
Lepiej nie myśleć. Tzn. dokładnie to twórcy standardu pomyśleli i o tym; program taki bezlitośnie idzie w buraki, w czym pomaga mu std::terminate(). Destruktorowi zatem bezwzględnie nie wolno w takich okolicznościach rzucać wyjątkiem. No dobrze, ale - jak wspomniałem - istnieją sytuacje, w których rzucanie wyjątkiem spod destruktora ma sens. Dobrze by więc było mieć jakieś narzędzie, które pozwala nam na jednoznaczne stwierdzenie, czy destruktor wywołał się w wyniku nadepnięcia na delete lub normalnego zakończenia kontekstu, czy też został wywołany z powodu zwijania stosu w wyniku propagacji wyjątku.
I takie coś istnieje. Nazywa się std::uncaught_exception() (z nagłówka <exception>). Typem zwracanym jest bool i zwraca ona true, jeśli jesteśmy w trakcie propagacji wyjątku. To oznacza, że destruktor, który wywołuje coś, co mogłoby rzucić wyjątkiem, albo sam zamierza nim rzucić, powinien sprawdzać za pomocą tej funkcji, czy na pewno może to zrobić. Oczywiście kwestia tego, czy jest to obowiązkowe, należy do programisty; jeśli jest pewien, że wywołanie danego destruktora nie zajdzie w wyniku propagacji wyjątku, nie musi tego sprawdzać, ale oczywiście powinien być tego świadomy, że takowe niebezpieczeństwo istnieje.
Podsumowanie wyjątków
Osobiście zachęcam do eksperymentowania z wyjątkami, aczkolwiek - jak we wszystkich zaawansowanych właściwościach - zalecam umiar i rozsądek. Uzyskałem swego czasu nawet pozytywne rezultaty tłumaczenia sygnałów na wyjątki (tzn. jedna funkcja obsługiwała wszystkie sygnały, ale w zależności od numeru rzucała odpowiednim wyjątkiem), choć Bjarne Stroustrup niezbyt przychylnie się odnosi do tego pomysłu. Tu kwestia jest zresztą o tyle ciekawa, że o ile sygnały nie mają ustalonej semantyki (da się więc zaimplementować semantykę wznowienia), o tyle wyjątki w C++ wspierają wyłacznie semantykę zakończenia (kwestia braku tej semantyki nie jest jednak w sygnałach niczym sensownym: w języku C obsługę przyjęcia sygnału należy wykonać przez longjmp, gdyż dany sygnał zazwyczaj i tak dyskwalifikuje bieżący kontekst z wykonywania).
Nie zalecam oczywiście stosowania wyjątków do przekazywania danych, choć sam stosowałem je jako normalny element programu, obsługujący sytuację choć wyjątkową, to jednak w stu procentach oczekiwaną. Nie należy obawiać się ich używania, zwłaszcza że w wielu zastosowaniach będą nieocenione.