Dlaczego specyfikacje wyjątków są złe?

50

Ponad 10 lat temu w szkole uczyli cię używania specyfikatorów wyjątków. Ponieważ moje doświadczenie jest jednym z nich, programiści Torvaldish C, którzy uparcie unikają C ++, chyba że są do tego zmuszeni, trafiam tylko do C ++ sporadycznie, a kiedy to robię, nadal używam specyfikatorów wyjątków, ponieważ tego się nauczono.

Jednak większość programistów C ++ marszczy brwi w stosunku do specyfikatorów wyjątków. Przeczytałem debatę i argumenty różnych guru C ++, takich jak te . O ile rozumiem, sprowadza się do trzech rzeczy:

  1. Specyfikatory wyjątków używają systemu typów, który jest niezgodny z resztą języka („system typów cieni”).
  2. Jeśli twoja funkcja ze specyfikatorem wyjątku wyrzuci cokolwiek innego niż to, co określiłeś, program zostanie zakończony w zły, nieoczekiwany sposób.
  3. Specyfikatory wyjątków zostaną usunięte w nadchodzącym standardzie C ++.

Czy coś tu brakuje, czy to z tych wszystkich powodów?

Moje własne opinie:

Odnośnie 1): Więc co. C ++ jest prawdopodobnie najbardziej niespójnym językiem programowania, jaki kiedykolwiek stworzono, pod względem składniowym. Mamy makra, goto / etykiety, hordę (skarb?) O zachowaniu niezdefiniowanym / nieokreślonym / implementacji, źle zdefiniowane typy liczb całkowitych, wszystkie reguły promocji typu domyślnego, słowa kluczowe ze specjalnymi przypadkami, takie jak przyjaciel, auto , zarejestruj się, wyraźne ... I tak dalej. Ktoś prawdopodobnie mógłby napisać kilka grubych książek o całej dziwności w C / C ++. Dlaczego więc ludzie reagują na tę szczególną niekonsekwencję, która jest drobną wadą w porównaniu z wieloma innymi znacznie bardziej niebezpiecznymi cechami języka?

Odnośnie 2): Czy to nie moja własna odpowiedzialność? Jest tak wiele innych sposobów, aby napisać fatalny błąd w C ++, dlaczego ten konkretny przypadek jest jeszcze gorszy? Zamiast pisać, throw(int)a następnie rzucać Crash_t, mogę równie dobrze twierdzić, że moja funkcja zwraca wskaźnik do int, a następnie utworzyć dziki, wyraźny typecast i zwrócić wskaźnik do Crash_t. Duchem C / C ++ zawsze było pozostawienie większości odpowiedzialności programistom.

A co z zaletami? Najbardziej oczywiste jest to, że jeśli twoja funkcja próbuje jawnie wyrzucić dowolny typ inny niż określony, kompilator wyświetli błąd. Uważam, że standard jest jasny w tym zakresie (?). Błędy będą występować tylko wtedy, gdy funkcja wywołuje inne funkcje, które z kolei generują niewłaściwy typ.

Pochodząc ze świata deterministycznych, osadzonych programów C, z pewnością wolałbym wiedzieć dokładnie, co rzuci na mnie funkcja. Jeśli jest coś w tym języku, dlaczego tego nie użyć? Alternatywami wydają się być:

void func() throw(Egg_t);

i

void func(); // This function throws an Egg_t

Myślę, że istnieje duża szansa, że ​​osoba dzwoniąca zignoruje / zapomni wdrożyć try-catch w drugim przypadku, a mniej w pierwszym przypadku.

Jak rozumiem, jeśli jedna z tych dwóch form zdecyduje się nagle rzucić inny rodzaj wyjątku, program się zawiesi. W pierwszym przypadku, ponieważ nie wolno rzucać innego wyjątku, w drugim przypadku, ponieważ nikt nie spodziewał się, że wyrzuci SpanishInquisition_t, a zatem to wyrażenie nie jest rejestrowane tam, gdzie powinno być.

W przypadku tego ostatniego, złapanie w ostateczności (...) na najwyższym poziomie programu nie wydaje się wcale lepsze niż awaria programu: „Hej, gdzieś w twoim programie coś rzuciło dziwny, nieobsługiwany wyjątek . ” Nie możesz odzyskać programu, gdy jesteś tak daleko od miejsca zgłoszenia wyjątku, jedyne, co możesz zrobić, to wyjść z programu.

A z punktu widzenia użytkownika nie obchodzi ich to, że otrzymają z systemu operacyjnego złą wiadomość z komunikatem „Program zakończony. Blablabla pod adresem 0x12345” lub złą wiadomość z twojego programu z informacją „Nieobsługiwany wyjątek: mojaklasa. func.something ”. Błąd jest nadal obecny.


W nadchodzącym standardzie C ++ nie będę miał innej opcji, jak zrezygnować ze specyfikatorów wyjątków. Ale wolałbym raczej wysłuchać solidnego argumentu, dlaczego są źli, niż „Jego Świątobliwość to stwierdził i tak jest”. Być może jest więcej argumentów przeciwko nim niż te, które wymieniłem, a może jest ich więcej, niż zdaję sobie sprawę?


źródło
30
Kusi mnie, by głosić to jako „rant, przebrany za pytanie”. Zadajesz trzy ważne uwagi na temat E.specs, ale dlaczego przeszkadzasz nam swoim C ++ - jest tak denerwujące i podobne do C-lepszych ramblings?
Martin Ba
10
@Martin: Ponieważ chcę podkreślić, że jestem stronniczy i nie jestem na bieżąco ze wszystkimi szczegółami, ale także, że patrzę na ten język naiwnymi i / lub jeszcze nie zrujnowanymi oczami. Ale także, że C i C ++ są już niesamowicie wadliwymi językami, więc jeden błąd mniej więcej tak naprawdę nie ma znaczenia. Post był w rzeczywistości znacznie gorszy, zanim go zredagowałem :) Argumenty przeciwko specyfikatorom wyjątków są również dość subiektywne, dlatego trudno jest je omawiać bez samodzielnego uzyskania subiektywności.
SpanishInquisition_t! Wesoły! Osobiście byłem pod wrażeniem użycia wskaźnika ramki stosu do generowania wyjątków i wydaje się, że może to uczynić kod o wiele czystszym. Jednak tak naprawdę nigdy nie pisałem kodu z wyjątkami. Nazywaj mnie staroświeckim, ale zwracane wartości działają dobrze dla mnie.
Shahbaz
@Shahbaz Jak można przeczytać między wierszami, jestem też dość staromodny, ale tak naprawdę nigdy nie kwestionowałem, czy same wyjątki są dobre, czy złe.
2
Czas przeszły rzut jest rzucany , a nie rzucany . To mocny czasownik.
TRiG,

Odpowiedzi:

48

Specyfikacje wyjątków są złe, ponieważ są słabo egzekwowane, a zatem nie osiągają zbyt wiele, a także są złe, ponieważ zmuszają środowisko wykonawcze do sprawdzenia nieoczekiwanych wyjątków, aby mogły zakończyć działanie (), zamiast wywoływać UB , może to zmarnować znaczną część wydajności.

Podsumowując, specyfikacje wyjątków nie są wystarczająco egzekwowane w języku, aby kod stał się bezpieczniejszy, a wdrożenie ich zgodnie ze specyfikacją spowodowało duży spadek wydajności.

DeadMG
źródło
2
Ale sprawdzenie w czasie wykonywania nie jest winą specyfikacji wyjątku jako takiej, ale raczej jakiejkolwiek części standardu, która stwierdza, że ​​powinno nastąpić zakończenie, jeśli zostanie rzucony niepoprawny typ. Czy nie byłoby mądrzej po prostu usunąć wymaganie prowadzące do tej kontroli dynamicznej i pozwolić kompilatorowi ocenić wszystko statycznie? Wygląda na to, że RTTI jest tutaj prawdziwym winowajcą, czy ...?
7
@Lundin: nie jest możliwe statyczne ustalenie wszystkich wyjątków, które można by wyrzucić z funkcji w C ++, ponieważ język pozwala na dowolne i niedeterministyczne zmiany w przepływie sterowania w czasie wykonywania (na przykład za pomocą wskaźników funkcji, metod wirtualnych i jak). Wdrożenie statycznego sprawdzania wyjątków podczas kompilacji, takiego jak Java, wymagałoby fundamentalnych zmian w języku i wyłączenia dużej ilości C-kompatybilności.
3
@OrbWeaver: Myślę, że sprawdzanie statyczne jest całkiem możliwe - specyfikacja wyjątku byłaby częścią specyfikacji funkcji (jak wartość zwracana), a wskaźniki funkcji, zastępowania wirtualne itp. Musiałyby mieć zgodne specyfikacje. Głównym zastrzeżeniem byłoby to, że jest to funkcja „wszystko albo nic” - funkcje ze statycznymi specyfikacjami wyjątków nie mogły wywoływać funkcji starszych. (Wywołanie funkcji C byłoby w porządku, ponieważ nie mogą niczego rzucać).
Mike Seymour,
2
@Lundin: Specyfikacje wyjątków nie zostały usunięte, tylko przestarzałe. Starszy kod, który ich używa, nadal będzie ważny.
Mike Seymour
1
@Lundin: Stare wersje C ++ ze specyfikacjami wyjątków są doskonale kompatybilne. Nie będą się kompilować ani nie będą działać nieoczekiwanie, jeśli kod był wcześniej ważny.
DeadMG
18

Jednym z powodów, dla których nikt ich nie używa, jest to, że twoje podstawowe założenie jest błędne:

„Najbardziej oczywistą [zaletą] jest to, że jeśli twoja funkcja próbuje jawnie wyrzucić dowolny typ inny niż określony, kompilator wyświetli błąd.”

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

Ten program kompiluje się bez błędów i ostrzeżeń .

Ponadto, mimo że wyjątek zostałby wychwycony , program nadal się kończy.


Edycja: aby odpowiedzieć na pytanie w pytaniu, catch (...) (lub lepiej, catch (std :: exception &) lub inna klasa podstawowa, a następnie catch (...)) jest nadal przydatne, nawet jeśli nie dokładnie wiedzieć, co poszło nie tak.

Weź pod uwagę, że użytkownik nacisnął przycisk menu „Zapisz”. Moduł obsługi przycisku menu zaprasza aplikację do zapisania. Może się to nie udać z wielu powodów: plik znajdował się w zasobie sieciowym, który zniknął, był plik tylko do odczytu i nie można go zapisać itp. Ale program obsługi nie obchodzi, dlaczego coś się nie powiedzie; obchodzi go tylko to, czy mu się to udało, czy nie. Jeśli się powiedzie, wszystko jest dobrze. Jeśli się nie powiedzie, może poinformować użytkownika. Dokładny charakter wyjątku jest nieistotny. Ponadto, jeśli napiszesz odpowiedni, bezpieczny dla wyjątków kod, oznacza to, że każdy taki błąd może rozprzestrzeniać się w twoim systemie bez obniżania go - nawet dla kodu, który nie przejmuje się błędem. Ułatwia to rozbudowę systemu. Na przykład teraz oszczędzasz przez połączenie z bazą danych.

Kaz Dragon
źródło
4
@Lundin skompilowany bez ostrzeżeń. I nie, nie możesz. Nie niezawodnie. Możesz wywoływać za pomocą wskaźników funkcji lub może to być wirtualna funkcja składowa, a klasa pochodna generuje wyjątek pochodny (o którym klasa podstawowa prawdopodobnie nie może wiedzieć).
Kaz Dragon
Pech. Embarcadero / Borland daje ostrzeżenie: „wyjątek rzucenia narusza specyfikator wyjątku”. Najwyraźniej GCC nie. Nie ma jednak powodu, dla którego nie mogli wprowadzić ostrzeżenia. Podobnie narzędzie do analizy statycznej może łatwo znaleźć ten błąd.
14
@Lundin: Nie zaczynaj kłócić się i powtarzać fałszywych informacji. Twoje pytanie brzmiało już jak twierdzenie, a twierdzenie, że są fałszywe, nie pomaga. Narzędzie do analizy statycznej NIE MOŻE znaleźć, jeśli tak się stanie; zrobienie tego wymagałoby rozwiązania problemu zatrzymania. Co więcej, musiałby przeanalizować cały program i bardzo często całe źródło nie jest dostępne.
David Thornley
1
@Lundin to samo narzędzie do analizy statycznej może powiedzieć, które zgłoszone wyjątki opuszczają Twój stos, więc w tym przypadku użycie specyfikacji wyjątków nadal nie daje nic poza potencjalnym fałszywie ujemnym wynikiem, który doprowadziłby twój program do sytuacji, w której mógłbyś obsłużyć przypadek błędu w o wiele ładniejszy sposób (np. ogłaszanie awarii i kontynuowanie działania: na przykład patrz druga połowa mojej odpowiedzi).
Kaz Dragon
1
@Lundin Dobre pytanie. Odpowiedź jest taka, że ​​generalnie nie wiesz, czy wywoływane funkcje spowodują wyjątki, więc bezpiecznym założeniem jest to, że wszystkie one wykonują. Gdzie je złapać, to pytanie, na które odpowiedziałem w stackoverflow.com/questions/4025667/… . W szczególności procedury obsługi zdarzeń tworzą naturalne granice modułów. Umożliwienie wyjątków ucieczki do ogólnego systemu okien / zdarzeń (np.) Będzie karane. Przewodnik powinien je złapać, jeśli program ma tam przetrwać.
Kaz Dragon
17

Ten wywiad z Anders Hejlsberg jest dość sławny. W nim wyjaśnia, dlaczego zespół projektowy C # odrzucił sprawdzone wyjątki. Krótko mówiąc, istnieją dwa główne powody: możliwość aktualizacji i skalowalność.

Wiem, że OP koncentruje się na C ++, podczas gdy Hejlsberg omawia C #, ale kwestie, które wysuwa Hejlsberg, doskonale pasują również do C ++.

CesarGon
źródło
5
„punkty, które wysuwa Hejlsberg, doskonale pasują również do C ++”. Źle! Sprawdzone wyjątki zaimplementowane w Javie zmuszają osobę dzwoniącą do radzenia sobie z nimi (i / lub osoby dzwoniącej itp.). Specyfikacje wyjątków w C ++ (choć dość słabe i bezużyteczne) dotyczą tylko pojedynczej „granicy wywołania funkcji” i nie „propagują stosu wywołań”, jak sprawdzone wyjątki.
Martin Ba
3
@Martin: Zgadzam się z Tobą w sprawie zachowania specyfikacji wyjątków w C ++. Ale moje zdanie, które cytujesz „punkty, które wysuwa Hejlsberg, również doskonale odnoszą się do C ++”, odnosi się raczej do punktów, które wysuwa Hejlsberg, niż do specyfikacji C ++. Innymi słowy, mówię tu o możliwości aktualizacji i skalowalności programu, a nie o propagacji wyjątków.
CesarGon 17.10.11
3
Dość wcześnie nie zgodziłem się z Hejlsbergiem: „Obawy, jakie mam na temat sprawdzonych wyjątków, dotyczą kajdanek, które kładą na programistach. Widzisz, jak programiści wybierają nowe interfejsy API, które zawierają wszystkie te klauzule wyrzucania, a potem widzisz, jak skomplikowany jest ich kod i zdajesz sobie sprawę sprawdzone wyjątki nic im nie pomagają. To taki dyktatorski projektant API, który mówi ci, jak postępować z wyjątkami ”. --- Wyjątki są sprawdzane czy nie, nadal musisz obsługiwać wyjątki zgłaszane przez API, do którego dzwonisz. Sprawdzone wyjątki sprawiają, że jest to po prostu wyraźne.
quant_dev 18.10.11
5
@quant_dev: Nie, nie musisz obsługiwać wyjątków zgłaszanych przez API, do którego dzwonisz. Idealnie ważna opcja jest nie obsługiwać wyjątki i pozwól im się bańkę stos wywołań dla rozmówca obsłużyć.
CesarGon,
4
@CesarGon Nieobsługiwanie wyjątków powoduje również wybór sposobu ich obsługi.
quant_dev,