Czy powinienem dziedziczyć po std :: wyjątek?

98

Widziałem co najmniej jedno wiarygodne źródło (klasa C ++, którą wybrałem), które zalecają, aby klasy wyjątków specyficzne dla aplikacji w C ++ dziedziczyły z std::exception. Nie mam jasności co do zalet tego podejścia.

W C # powody dziedziczenia z ApplicationExceptionsą jasne: dostajesz garść użytecznych metod, właściwości i konstruktorów i po prostu musisz dodać lub zastąpić to, czego potrzebujesz. Z std::exceptionWydaje się, że wszystko można dostać to what()metoda aby przesłonić, które można równie dobrze tworzyć samemu.

Więc jakie są korzyści z używania std::exceptionjako klasy bazowej dla mojej klasy wyjątków specyficznej dla aplikacji? Czy istnieją dobre powody, aby nie dziedziczyć std::exception?

John M Gant
źródło
Możesz
rzucić
2
Chociaż na marginesie nie ma związku z konkretnym pytaniem, zajęcia w C ++ , które bierzesz, niekoniecznie muszą być wiarygodnymi źródłami dobrych praktyk, tylko same w sobie.
Christian Rau

Odpowiedzi:

69

Główną zaletą jest to, że kod przy użyciu klasy nie muszą znać dokładny typ, co cię throww nim, ale może po prostu . Edycja: jak zauważyli Martin i inni, tak naprawdę chcesz wyprowadzić z jednej z podklas zadeklarowanych w nagłówku.catchstd::exception

std::exception<stdexcept>

Nikolai Fetissov
źródło
21
Nie ma możliwości przekazania wiadomości do std :: wyjątek. std :: runtime_error akceptuje ciąg znaków i pochodzi od std :: wyjątek.
Martin York
14
Nie powinieneś przekazywać komunikatu do konstruktora typu wyjątku (pamiętaj, że komunikaty muszą być zlokalizowane). Zamiast tego zdefiniuj typ wyjątku, który semantycznie kategoryzuje błąd, zapisz w obiekcie wyjątku to, czego potrzebujesz, aby sformatować przyjazną dla użytkownika wiadomość , a następnie zrób to w miejscu połowu.
Emil,
std::exceptionjest zdefiniowane w <exception>nagłówku ( dowód ). -1
Alexander Shukaev
5
Więc o co ci chodzi? Definicja implikuje deklarację, co według ciebie jest nie tak?
Nikolai Fetissov
40

Problem std::exceptionpolega na tym, że nie ma konstruktora (w wersjach zgodnych ze standardem), który akceptuje komunikat.

W rezultacie wolę czerpać z std::runtime_error. Pochodzi z, std::exceptionale jego konstruktory umożliwiają przekazanie C-String lub a std::stringdo konstruktora, który zostanie zwrócony (jako a char const*), gdy what()zostanie wywołany.

Martin York
źródło
11
Nie jest dobrym pomysłem formatowanie przyjaznej dla użytkownika wiadomości w miejscu rzutu, ponieważ wiązałoby to kod niższego poziomu z funkcjonalnością lokalizacji i tak dalej. Zamiast tego przechowuj w obiekcie wyjątku wszystkie istotne informacje i pozwól, aby witryna catch sformatowała przyjazny dla użytkownika komunikat na podstawie typu wyjątku i przenoszonych danych.
Emil,
16
@ Emil: Jasne, jeśli jesteś wyjątkiem, przenosisz komunikaty wyświetlane przez użytkownika, chociaż generalnie służą one tylko do rejestrowania. W witrynie rzutów nie masz kontekstu do zbudowania wiadomości użytkownika (ponieważ i tak może to być biblioteka). Witryna catch będzie miała więcej kontekstu i może wygenerować odpowiednią wiadomość.
Martin York,
3
Nie wiem, skąd pomysł, że wyjątki służą tylko do logowania. :)
Emil
1
@Emil: Przeszliśmy już przez ten argument. Tam, gdzie wskazałem, że nie o to chodzi w wyjątku. Zrozumienie tematu wydaje się dobre, ale argumentacja jest błędna. Komunikaty o błędach generowane dla użytkownika są zupełnie inne niż komunikaty dziennika dla programisty.
Martin York,
1
@Emil. Ta dyskusja trwa od roku. W tym miejscu utwórz własne pytanie. Jeśli o mnie chodzi, Twoje argumenty są po prostu błędne (i na podstawie głosów ludzie zwykle się ze mną zgadzają). Zobacz: Jaka jest „dobra liczba” wyjątków do zaimplementowania w mojej bibliotece? i czy łapanie ogólnych wyjątków to naprawdę zła rzecz? Nie podałeś żadnych informacji nw od czasu poprzedniej diatryby.
Martin York
17

Przyczyną dziedziczenia std::exceptionjest „standardowa” klasa bazowa dla wyjątków, więc jest rzeczą naturalną, że na przykład inne osoby w zespole mogą się tego spodziewać i złapać podstawę std::exception.

Jeśli szukasz wygody, możesz odziedziczyć po std::runtime_errortym, co zapewnia std::stringkonstruktor.

Aleksei Potov
źródło
2
Wyprowadzanie z dowolnego innego standardowego typu wyjątku z wyjątkiem std :: wyjątek prawdopodobnie nie jest dobrym pomysłem. Problem polega na tym, że wywodzą się one ze std ::ception nie-wirtualnie, co może prowadzić do subtelnych błędów obejmujących wielokrotne dziedziczenie, w których catch (std :: wyjątek &) może po cichu nie przechwycić wyjątku z powodu konwersji do std :: wyjątek jest dwuznaczny.
Emil
2
Przeczytałem artykuł o doładowaniu na ten temat. Wydaje się to rozsądne z czysto logicznego punktu widzenia. Ale nigdy nie widziałem MH w wyjątkach na wolności. Nie rozważałbym też używania MH w wyjątkach, więc wydaje się, że jest to młot kowalski, aby naprawić nieistniejący problem. Gdyby to był prawdziwy problem, jestem pewien, że podjęlibyśmy działania w tej sprawie ze strony komisji normalizacyjnej w celu naprawienia tak oczywistej usterki. Więc moim zdaniem std :: runtime_error (i rodzina) są nadal całkowicie akceptowalnymi wyjątkami do wyrzucenia lub wyprowadzenia.
Martin York,
5
@Emil: Wyprowadzanie z innych standardowych wyjątków std::exceptionto doskonały pomysł. Nie powinieneś dziedziczyć z więcej niż jednego.
Mooing Duck,
@MooingDuck: Jeśli wywodzisz się z więcej niż jednego standardowego typu wyjątku, w rezultacie std :: wyjątek zostanie wyprowadzony (nie wirtualnie) wiele razy. Zobacz boost.org/doc/libs/release/libs/exception/doc/… .
Emil,
1
@Emil: To wynika z tego, co powiedziałem.
Mooing Duck
13

Kiedyś brałem udział w porządkowaniu dużej bazy kodu, w której poprzedni autorzy wrzucali ints, HRESULTS, std :: string, char *, losowe klasy ... wszędzie różne rzeczy; wystarczy podać typ i prawdopodobnie został gdzieś wyrzucony. I w ogóle nie ma wspólnej klasy bazowej. Uwierz mi, sprawy były znacznie uporządkowane, gdy doszliśmy do punktu, w którym wszyscy rzucani mieli wspólną podstawę, którą mogliśmy złapać i wiedzieli, że nic nie minie. Więc zrób sobie (i tym, którzy będą musieli dbać o Twój kod w przyszłości) przysługę i rób to w ten sposób od samego początku.

Timday
źródło
12

Powinieneś odziedziczyć po boost :: wyjątek . Zapewnia o wiele więcej funkcji i dobrze zrozumiałe sposoby przenoszenia dodatkowych danych ... oczywiście, jeśli nie używasz Boost , zignoruj ​​tę sugestię.

James Schek
źródło
5
Należy jednak zauważyć, że sam boost :: wyjątek nie pochodzi od std :: wyjątek. Nawet jeśli wyprowadzasz z boost :: wyjątek, powinieneś także wyprowadzić ze std :: wyjątek (jako osobna uwaga, zawsze gdy wyprowadzasz z typów wyjątków, powinieneś używać dziedziczenia wirtualnego.)
Emil
10

Tak, powinieneś pochodzić z std::exception.

Inni odpowiedzieli, że std::exceptionma problem polegający na tym, że nie możesz przekazać do niego wiadomości tekstowej, jednak generalnie nie jest dobrym pomysłem próba sformatowania wiadomości użytkownika w miejscu rzutu. Zamiast tego użyj obiektu wyjątku, aby przetransportować wszystkie istotne informacje do miejsca łapania, które może następnie sformatować przyjazny dla użytkownika komunikat.

Emil
źródło
6
+1, słuszna uwaga. Zarówno z oddzielenia obaw, jak iz perspektywy i18n, zdecydowanie lepiej pozwolić warstwie prezentacji skonstruować przekaz użytkownika.
John M Gant
@JohnMGant i Emil: Ciekawe, czy możesz wskazać konkretny przykład, jak można to zrobić. Rozumiem, że można czerpać std::exceptioni przenosić informacje o wyjątku. Ale kto będzie odpowiedzialny za tworzenie / formatowanie komunikatu o błędzie? ::what()Funkcja lub coś innego?
alfC
1
Sformatowałbyś wiadomość w witrynie catch, najprawdopodobniej na poziomie aplikacji (a nie biblioteki). Łapiesz go według typu, następnie sondujesz w poszukiwaniu informacji, a następnie lokalizujesz / formatujesz odpowiednią wiadomość użytkownika.
Emil
9

Powodem, dla którego możesz chcieć dziedziczyć z, std::exceptionjest to, że pozwala ci zgłosić wyjątek przechwycony zgodnie z tą klasą, tj .:

class myException : public std::exception { ... };
try {
    ...
    throw myException();
}
catch (std::exception &theException) {
    ...
}
SingleNegationElimination
źródło
6

Jest jeden problem z dziedziczeniem, o którym powinieneś wiedzieć, to cięcie obiektów. Podczas pisania throw e;wyrażenia rzutowego inicjuje obiekt tymczasowy, zwany obiektem wyjątku, którego typ jest określany przez usunięcie wszelkich kwalifikatorów cv najwyższego poziomu z typu statycznego operandu throw. To może nie być to, czego się spodziewasz. Przykład problemu, który możesz znaleźć tutaj .

To nie jest argument przeciwko dziedziczeniu, to po prostu informacja „trzeba wiedzieć”.

Kirill V. Lyadvinsky
źródło
3
Myślę, że na wynos jest to „rzuć”; jest zły i „rzuca”; jest ok
James Schek
2
Tak, throw;jest ok, ale nie jest oczywiste, że należy coś takiego napisać.
Kirill V. Lyadvinsky
1
Jest to szczególnie bolesne dla programistów Java, gdzie ponowne wywołanie odbywa się za pomocą polecenia „throw e;”
James Schek
Rozważ tylko pochodzenie z abstrakcyjnych typów podstawowych. Wyłapywanie abstrakcyjnego typu podstawowego według wartości da ci błąd (uniknięcie cięcia na plasterki); wychwycenie abstrakcyjnego typu podstawowego przez odniesienie, a następnie próba wrzucenia jego kopii spowoduje błąd (ponownie unikając wycinania).
Emil
Można również rozwiązać ten problem poprzez dodanie funkcji Członka, raise()jako takie: virtual void raise() { throw *this; }. Pamiętaj tylko, aby nadpisać to w każdym wyprowadzonym wyjątku.
Justin Time - Przywróć Monikę
5

Różnica: std :: runtime_error vs std :: wyjątek ()

To, czy powinieneś po nim odziedziczyć, czy nie, zależy od Ciebie. Standard std::exceptioni jego standardowe potomkowie proponują jedną możliwą strukturę hierarchii wyjątków (podział na logic_errorsubhierarchy i runtime_errorsubhierarchy) oraz jeden możliwy interfejs obiektu wyjątku. Jeśli Ci się podoba - wykorzystaj. Jeśli z jakiegoś powodu potrzebujesz czegoś innego - zdefiniuj własną strukturę wyjątków.

Mrówka
źródło
3

Ponieważ język już zgłasza std :: wyjątek, i tak musisz go przechwycić, aby zapewnić przyzwoite raportowanie błędów. Równie dobrze możesz użyć tego samego haczyka we wszystkich nieoczekiwanych wyjątkach. Ponadto prawie każda biblioteka, która zgłasza wyjątki, mogłaby je wyprowadzić ze std :: wyjątek.

Innymi słowy, to albo

catch (...) {cout << "Unknown exception"; }

lub

catch (const std::exception &e) { cout << "unexpected exception " << e.what();}

A druga opcja jest zdecydowanie lepsza.


źródło
3

Jeśli wynikają wszystkie możliwe wyjątki std::exception, blok catch można łatwo catch(std::exception & e)i pewnie przechwycić wszystko.

Po przechwyceniu wyjątku możesz użyć tej whatmetody, aby uzyskać więcej informacji. C ++ nie obsługuje pisania typu kaczego, więc inna klasa z whatmetodą wymagałaby innego catch i innego kodu, aby jej użyć.

Mark Okup
źródło
7
nie podklasy std :: wyjątek, a następnie przechwytuj (std :: wyjątek e). Musisz złapać odwołanie do std :: wyjątek & (lub wskaźnik) albo wycinasz dane podklasy.
jmucchiello
1
To był głupi błąd - na pewno wiem lepiej. stackoverflow.com/questions/1095225/…
Oznacz okup
3

Pierwszym pytaniem jest, czy pochodzić z dowolnego standardowego typu wyjątku, czy nie. W ten sposób włącza się jeden program obsługi wyjątków dla wszystkich wyjątków bibliotek standardowych i własnych, ale zachęca również do takich programów obsługi typu catch-them-all. Problem polega na tym, że należy wychwytywać tylko wyjątki, które znamy. Na przykład w funkcji main () przechwytywanie wszystkich wyjątków std :: jest prawdopodobnie dobrą rzeczą, jeśli łańcuch what () zostanie zarejestrowany w ostateczności przed zakończeniem. Jednak gdzie indziej nie jest to dobry pomysł.

Kiedy już zdecydujesz, czy wyprowadzić ze standardowego typu wyjątku, czy nie, pojawia się pytanie, który powinien być podstawą. Jeśli Twoja aplikacja nie potrzebuje i18n, możesz pomyśleć, że formatowanie wiadomości w miejscu wywołania jest równie dobre, jak zapisywanie informacji i generowanie wiadomości w miejscu wywołania. Problem polega na tym, że sformatowana wiadomość może nie być potrzebna. Lepiej jest użyć leniwego schematu generowania wiadomości - być może z wstępnie przydzieloną pamięcią. Następnie, jeśli wiadomość jest potrzebna, zostanie wygenerowana przy dostępie (i prawdopodobnie buforowana w obiekcie wyjątku). W związku z tym, jeśli komunikat jest generowany podczas rzucania, wówczas jako klasa podstawowa wymagana jest podstawowa klasa std :: wyjątek, taka jak std :: runtime_error. Jeśli wiadomość jest generowana leniwie, wówczas odpowiednią podstawą jest std ::ception.

Obrabować
źródło
0

Innym powodem wyjątków dla podklas jest lepszy aspekt projektowania podczas pracy z dużymi zamkniętymi systemami. Możesz go ponownie użyć do takich rzeczy, jak komunikaty walidacyjne, zapytania użytkowników, krytyczne błędy kontrolera i tak dalej. Zamiast przepisać lub ponownie podłączyć wszystkie swoje komunikaty walidacyjne, takie jak komunikaty, możesz po prostu „przechwycić” je w głównym pliku źródłowym, ale wyrzucić błąd w dowolnym miejscu w całym zestawie klas.

Np. krytyczny wyjątek zakończy działanie programu, błąd walidacji wyczyści tylko stos, a zapytanie użytkownika zada pytanie użytkownikowi końcowemu.

Zrobienie tego w ten sposób oznacza również, że możesz ponownie użyć tych samych klas, ale na różnych interfejsach. np. aplikacja Windows może korzystać z okna komunikatu, usługa sieciowa pokaże html, a system raportowania go zaloguje i tak dalej.

James Burnby
źródło
0

Chociaż to pytanie jest dość stare i zostało już wiele odpowiedzi, chcę tylko dodać uwagę na temat poprawnej obsługi wyjątków w C ++ 11, ponieważ ciągle brakuje mi tego w dyskusjach na temat wyjątków:

Użyj std::nested_exceptionistd::throw_with_nested

Jest to opisane w StackOverflow tutaj i tutaj , w jaki sposób można uzyskać śledzenie wstecz dla wyjątków w kodzie bez potrzeby debugera lub kłopotliwego rejestrowania, po prostu pisząc odpowiednią procedurę obsługi wyjątków, która ponownie wyrzuci zagnieżdżone wyjątki.

Ponieważ możesz to zrobić z każdą pochodną klasą wyjątków, możesz dodać wiele informacji do takiego śledzenia wstecznego! Możesz również rzucić okiem na moje MWE na GitHub , gdzie ślad cofania wyglądałby mniej więcej tak:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

Nie musisz nawet tworzyć podklasy std::runtime_error, aby uzyskać dużo informacji, gdy zostanie zgłoszony wyjątek.

Jedyną korzyścią, jaką widzę w tworzeniu podklas (zamiast po prostu używać std::runtime_error), jest to, że program obsługi wyjątków może przechwycić niestandardowy wyjątek i zrobić coś specjalnego. Na przykład:

try
{
  // something that may throw
}
catch( const MyException & ex )
{
  // do something specialized with the
  // additional info inside MyException
}
catch( const std::exception & ex )
{
  std::cerr << ex.what() << std::endl;
}
catch( ... )
{
  std::cerr << "unknown exception!" << std::endl;
}
GPMueller
źródło