Wyjątkiem isocpp.org FAQ Zjednoczone
Nie używaj rzutu, aby wskazać błąd kodowania podczas używania funkcji. Użyj aser lub innego mechanizmu, aby albo wysłać proces do debugera, albo zawiesić proces i zebrać zrzut awaryjny dla programisty do debugowania.
Z drugiej strony standardowa biblioteka definiuje std :: logic_error i wszystkie jego pochodne, które wydają mi się, że powinny zajmować się, oprócz innych rzeczy, błędami programistycznymi. Czy przekazanie pustego ciągu do std :: stof (spowoduje wyrzucenie niepoprawnego argumentu) nie jest błędem programowania? Czy przekazanie ciągu zawierającego inne znaki niż „1” / „0” do std :: bitset (spowoduje wyrzucenie niepoprawnego argumentu) nie jest błędem programowania? Czy wywołanie std :: bitset :: set z niepoprawnym indeksem (spowoduje wyrzucenie_zakresu) nie jest błędem programowania? Jeśli tak nie jest, to jaki błąd programowy można sprawdzić? Konstruktor łańcuchowy std :: bitset istnieje tylko od wersji C ++ 11, więc powinien zostać zaprojektowany z idiomatycznym użyciem wyjątków. Z drugiej strony kazałem ludziom mówić, że logic_error w zasadzie nie powinien być w ogóle używany.
Inną zasadą, która często pojawia się z wyjątkami, jest „stosowanie wyjątków tylko w wyjątkowych okolicznościach”. Ale w jaki sposób funkcja biblioteczna ma wiedzieć, które okoliczności są wyjątkowe? W przypadku niektórych programów niemożność otwarcia pliku może być wyjątkowa. Dla innych niemożność przydzielenia pamięci może nie być wyjątkowa. Między nimi jest setki przypadków. Nie możesz utworzyć gniazda? Nie możesz się połączyć ani zapisać danych w gnieździe lub pliku? Nie można przeanalizować danych wejściowych? Może być wyjątkowy, może nie być. Sama funkcja zdecydowanie nie może ogólnie wiedzieć, nie ma pojęcia, w jakim kontekście jest wywoływana.
Jak więc mam zdecydować, czy mam stosować wyjątki, czy nie dla określonej funkcji? Wydaje mi się, że jedynym faktycznie spójnym sposobem jest użycie ich do każdej obsługi błędów lub do niczego. A jeśli korzystam ze standardowej biblioteki, wybór ten został dla mnie dokonany.
źródło
Odpowiedzi:
Po pierwsze, czuję się zobowiązany do podkreślenia tego,
std::exception
a jego dzieci zostały zaprojektowane dawno temu. Istnieje wiele części, które prawdopodobnie (prawie na pewno) byłyby inne, gdyby zostały zaprojektowane dzisiaj.Nie zrozumcie mnie źle: istnieją części projektu, które sprawdziły się całkiem dobrze i są całkiem dobrymi przykładami projektowania hierarchii wyjątków dla C ++ (np. Fakt, że w przeciwieństwie do większości innych klas, wszystkie mają wspólne wspólny korzeń).
Patrząc konkretnie
logic_error
, mamy trochę zagadki. Z jednej strony, jeśli masz rozsądny wybór w tej sprawie, wskazana rada jest słuszna: ogólnie najlepiej jest zawieść tak szybko i głośno, jak to możliwe, aby można było ją debugować i poprawić.Na lepsze lub gorsze trudno jednak zdefiniować standardową bibliotekę wokół tego, co ogólnie należy zrobić. Jeśli zdefiniowałoby to zamknięcie programu (np. Wywołanie
abort()
) przy niepoprawnym wprowadzeniu danych, byłoby tak zawsze w takich okolicznościach - i faktycznie jest całkiem sporo okoliczności, w których prawdopodobnie nie jest to właściwe , przynajmniej we wdrożonym kodzie.Miałoby to zastosowanie w kodzie z (przynajmniej miękkimi) wymaganiami w czasie rzeczywistym i minimalną karą za niepoprawne wyjście. Rozważmy na przykład program do czatowania. Jeśli dekoduje niektóre dane głosowe i otrzyma nieprawidłowe dane wejściowe, istnieje duże prawdopodobieństwo, że użytkownik będzie o wiele szczęśliwszy z milisekundą zakłóceń na wyjściu niż program, który po prostu całkowicie się wyłączy. Podobnie podczas odtwarzania wideo, bardziej akceptowalne może być życie z produkcją niewłaściwych wartości dla niektórych pikseli dla klatki lub dwóch, niż wtedy, gdy program zostanie ostatecznie zamknięty, ponieważ strumień wejściowy został uszkodzony.
Jeśli chodzi o to, czy użyć wyjątków do zgłaszania niektórych rodzajów błędów: masz rację - ta sama operacja może kwalifikować się jako wyjątek, czy nie, w zależności od tego, w jaki sposób jest używana.
Z drugiej strony również się mylisz - korzystanie ze standardowej biblioteki nie musi (koniecznie) wymuszać na tobie tej decyzji. W przypadku otwierania pliku zwykle używasz iostream. Iostreams też nie są najnowszą i najlepszą konstrukcją, ale w tym przypadku mają rację: pozwalają ustawić tryb błędu, dzięki czemu możesz kontrolować, czy niepowodzenie otwarcia pliku spowoduje wygenerowanie wyjątku, czy nie. Tak więc, jeśli masz plik, który jest naprawdę niezbędny dla Twojej aplikacji, a nieotrzymanie go oznacza, że musisz podjąć poważne działania naprawcze, możesz zgłosić wyjątek, jeśli nie uda się otworzyć tego pliku. W przypadku większości plików, które spróbujesz otworzyć, jeśli nie istnieją lub są niedostępne, po prostu zawiodą (jest to ustawienie domyślne).
Jeśli chodzi o sposób, w jaki decydujesz: nie sądzę, że istnieje prosta odpowiedź. Na lepsze lub gorsze „wyjątkowe okoliczności” nie zawsze są łatwe do zmierzenia. Chociaż z pewnością istnieją przypadki, które łatwo rozstrzygnąć, muszą być [nie] wyjątkowe, jednak istnieją (i zapewne zawsze będą) przypadki, w których jest ono otwarte na pytania lub wymaga znajomości kontekstu, który jest poza domeną danej funkcji. W takich przypadkach warto rozważyć projekt mniej więcej podobny do tej części iostreams, w której użytkownik może zdecydować, czy awaria spowoduje zgłoszenie wyjątku, czy nie. Alternatywnie, całkowicie możliwe jest posiadanie dwóch oddzielnych zestawów funkcji (lub klas itp.), Z których jeden zgłosi wyjątki wskazujące na niepowodzenie, a drugi używa innych środków. Jeśli pójdziesz tą drogą,
źródło
Możesz w to nie wierzyć, ale cóż, różni koderzy C ++ nie zgadzają się. Dlatego FAQ mówi jedno, ale standardowa biblioteka się nie zgadza.
Często zadawane pytania zalecają awarię, ponieważ łatwiej będzie ją debugować. Jeśli ulegniesz awarii i zrzucisz rdzeń, będziesz mieć dokładny stan swojej aplikacji. Jeśli rzucisz wyjątek, stracisz wiele z tego stanu.
Standardowa biblioteka przyjmuje teorię, że umożliwienie koderowi wychwycenia i ewentualnej obsługi błędu jest ważniejsze niż debuggowanie.
Chodzi o to, że jeśli twoja funkcja nie wie, czy sytuacja jest wyjątkowa, nie powinna generować wyjątku. Powinien zwrócić stan błędu za pośrednictwem innego mechanizmu. Gdy osiągnie punkt w programie, w którym wie, że stan jest wyjątkowy, powinien zgłosić wyjątek.
Ale to ma swój problem. Jeśli funkcja zwróci stan błędu, pamiętaj, aby go nie sprawdzić, a błąd przejdzie bezgłośnie. To powoduje, że niektórzy rezygnują z wyjątków, które stanowią wyjątkową zasadę na rzecz zgłaszania wyjątków dla każdego rodzaju błędu.
Ogólnie rzecz biorąc, kluczową kwestią jest to, że różne osoby mają różne wyobrażenia o tym, kiedy wprowadzić wyjątki. Nie znajdziesz ani jednego spójnego pomysłu. Mimo że niektórzy ludzie będą dogmatycznie twierdzić, że ten lub inny sposób postępowania z wyjątkami, nie ma jednej uzgodnionej teorii.
Możesz zgłaszać wyjątki:
i znajdź w internecie kogoś, kto się z tobą zgadza. Musisz przyjąć styl, który Ci odpowiada.
źródło
Napisano wiele innych dobrych odpowiedzi, chcę tylko dodać krótki punkt.
Tradycyjna odpowiedź, zwłaszcza gdy napisano FAQ ISO C ++, głównie porównuje „wyjątek C ++” z „kodem powrotu w stylu C”. Trzecia opcja, „zwrócenie pewnego rodzaju wartości złożonej, np. A
struct
lubunion
, lub w dzisiejszych czasachboost::variant
lub (proponowane)std::expected
, nie jest rozważana.Przed C ++ 11 opcja „zwróć typ złożony” była zwykle bardzo słaba. Ponieważ nie było semantyki ruchu, więc kopiowanie elementów do struktury i poza nią było potencjalnie bardzo kosztowne. W tym momencie języka było niezwykle ważne, aby dostosować swój kod do RVO , aby uzyskać najlepszą wydajność. Wyjątki były jak prosty sposób na skuteczne zwrócenie typu złożonego, w przeciwnym razie byłoby to dość trudne.
IMO, po C ++ 11, opcja „zwróć dyskryminowaną unię”, podobna do idiomu
Result<T, E>
używanego obecnie w Rust, powinna być częściej stosowana w kodzie C ++. Czasami jest to naprawdę prostszy i wygodniejszy sposób wskazywania błędów. Z wyjątkami zawsze istnieje taka możliwość, że funkcje, które wcześniej nie rzucały, mogły nagle zacząć rzucać po refaktorze, a programiści nie zawsze tak dobrze dokumentują takie rzeczy. Gdy błąd jest wskazywany jako część wartości zwracanej w dyskryminowanym związku, znacznie zmniejsza szansę, że programista po prostu zignoruje kod błędu, co jest zwykłą krytyką obsługi błędów w stylu C.Zwykle
Result<T, E>
działa jak opcjonalne wzmocnienie. Możesz przetestować, używającoperator bool
, jeśli jest to wartość lub błąd. A następnie użyj say,operator *
aby uzyskać dostęp do wartości lub innej funkcji „get”. Zwykle dostęp nie jest zaznaczony, ze względu na szybkość. Ale możesz to zrobić tak, aby w kompilacji debugowania dostęp został sprawdzony, a asercja upewnia się, że rzeczywiście istnieje wartość, a nie błąd. W ten sposób każdy, kto nie sprawdzi poprawnie błędów, otrzyma twarde potwierdzenie, a nie jakiś bardziej podstępny problem.Dodatkową zaletą jest to, że w przeciwieństwie do wyjątków, gdy nie zostanie złapany, po prostu leci w górę stosu na dowolną odległość, w tym stylu, gdy funkcja zaczyna sygnalizować błąd tam, gdzie nie była wcześniej, nie można kompilować, chyba że kod jest zmieniany, aby go obsłużyć. Powoduje to, że problemy stają się głośniejsze - tradycyjny problem „nieprzechwyconego wyjątku” bardziej przypomina błąd czasu kompilacji niż błąd czasu wykonywania.
Stałem się wielkim fanem tego stylu. Zazwyczaj obecnie używam tego lub wyjątków. Ale staram się ograniczyć wyjątki do poważnych problemów. Dla czegoś takiego jak błąd analizy, próbuję
expected<T>
na przykład wrócić . Rzeczy, jakstd::stoi
iboost::lexical_cast
który rzucać C ++ wyjątek w przypadku niektórych stosunkowo niewielki problem „ciąg nie może być zamieniony na numer” wydają się w bardzo złym guście do mnie dzisiaj.źródło
std::expected
jest wciąż odrzuconą propozycją, prawda?exception_ptr
, czy po prostu chcesz użyć jakiegoś typu struktury lub czegoś tak.[[nodiscard]] attribute
będzie pomocne w tym podejściu do obsługi błędów, ponieważ zapewnia, że nie zignorujesz wyniku błędu przypadkowo.except_ptr
) musiałeś zgłosić wyjątek wewnętrznie. Osobiście uważam, że takie narzędzie powinno działać całkowicie niezależnie od egzekucji. Tylko uwaga.Jest to bardzo subiektywna kwestia, ponieważ jest częścią projektowania. A ponieważ design jest w gruncie rzeczy sztuką, wolę dyskutować o tych rzeczach niż debacie (nie mówię, że debatujesz).
Dla mnie wyjątkowe przypadki są dwojakiego rodzaju - te, które dotyczą zasobów i te, które dotyczą operacji krytycznych. To, co można uznać za krytyczne, zależy od danego problemu, aw wielu przypadkach od punktu widzenia programisty.
Niepowodzenie w pozyskiwaniu zasobów jest najlepszym kandydatem do zgłaszania wyjątków. Zasobem może być pamięć, plik, połączenie sieciowe lub cokolwiek innego, w zależności od problemu i platformy. Czy brak wydania zasobu wymaga wyjątku? Cóż, to znowu zależy. Nie zrobiłem nic, co nie spowodowało zwolnienia pamięci, więc nie jestem pewien co do tego scenariusza. Jednak usuwanie plików w ramach wydania zasobów może się nie powieść i dla mnie nie powiodło się, a ta awaria jest zwykle powiązana z innym procesem, który pozostawił otwarty w aplikacji wieloprocesowej. Wydaje mi się, że inne zasoby mogą ulec awarii podczas wydawania, tak jak plik, i zwykle przyczyną tego problemu jest usterka projektowa, więc naprawienie go byłoby lepsze niż zgłaszanie wyjątków.
Potem przychodzi aktualizacja zasobów. Ta kwestia jest, przynajmniej dla mnie, ściśle związana z aspektem krytycznych operacji aplikacji. Wyobraź sobie
Employee
klasę z funkcją,UpdateDetails(std::string&)
która modyfikuje szczegóły na podstawie podanego ciągu oddzielonego przecinkami. Podobnie jak w przypadku awarii pamięci, trudno mi sobie wyobrazić, że przypisanie wartości zmiennych składowych nie powiedzie się z powodu mojego braku doświadczenia w takich domenach, w których mogą się zdarzyć. Jednak funkcja,UpdateDetailsAndUpdateFile(std::string&)
która działa tak, jak wskazuje nazwa, może się nie powieść. To właśnie nazywam operacją krytyczną.Teraz musisz sprawdzić, czy tak zwana operacja krytyczna uzasadnia wyjątek. Mam na myśli, czy aktualizacja pliku dzieje się na końcu, tak jak w destruktorze, czy jest to po prostu wywołanie paranoiczne po każdej aktualizacji? Czy istnieje mechanizm rezerwowy, który regularnie zapisuje niepisane obiekty? Mówię tylko, że musisz ocenić krytyczność operacji.
Oczywiście istnieje wiele operacji krytycznych, które nie są powiązane z zasobami. Jeśli
UpdateDetails()
podane zostaną nieprawidłowe dane, nie zaktualizują one szczegółów, a błąd musi zostać zgłoszony, abyś mógł tutaj zgłosić wyjątek. Ale wyobraź sobie taką funkcjęGiveRaise()
. Teraz, jeśli wspomniany pracownik ma szczęście, że ma spiczastego włosa szefa i nie dostanie podwyżki (w kategoriach programowych wartość jakiejś zmiennej powstrzymuje to), funkcja w zasadzie zawiodła. Czy rzuciłbyś tutaj wyjątek? Mówię tylko, że musisz ocenić konieczność wyjątku.Dla mnie spójność zależy od mojego podejścia projektowego niż użyteczności moich zajęć. Chodzi mi o to, że nie myślę w kategoriach „wszystkie funkcje Get muszą to robić i wszystkie funkcje Update muszą to robić”, ale sprawdzam, czy dana funkcja odwołuje się do określonego pomysłu w moim podejściu. Na pierwszy rzut oka zajęcia mogą wyglądać na przypadkowe, ale ilekroć użytkownicy (głównie koledzy z innych zespołów) o to pytają, wyjaśnię i wydają się zadowoleni.
Widzę wielu ludzi, którzy w zasadzie zastępują zwracane wartości wyjątkami, ponieważ używają C ++, a nie C, i że daje to „dobry rozdział obsługi błędów” itp. I nakłaniają mnie do zaprzestania „mieszania” języków itp. Zazwyczaj trzymam się z dala od tacy ludzie.
źródło
Po pierwsze, jak stwierdzili inni, w C ++ nie ma tak wyraźnego podziału, głównie dlatego, że wymagania i ograniczenia są nieco bardziej zróżnicowane w C ++ niż w innych językach, szczególnie. C # i Java, które mają „podobne” problemy z wyjątkami.
Wystawię na przykładzie std :: stof:
Podstawowy kontrakt tej funkcji, jak widzę, polega na tym, że próbuje przekonwertować argument na liczbę zmiennoprzecinkową, a każde niepowodzenie jest zgłaszane przez wyjątek. Oba możliwe wyjątki wywodzą się z
logic_error
błędu programisty, ale nie w jego znaczeniu, ale w sensie „danych wejściowych nie można nigdy przekształcić w liczbę zmiennoprzecinkową”.Tutaj można powiedzieć, że a
logic_error
jest używane do wskazania, że biorąc pod uwagę to wejście (środowisko uruchomieniowe), zawsze jest błąd, aby spróbować je przekonwertować - ale zadaniem tej funkcji jest ustalenie tego i powiedzenie (poprzez wyjątek).Uwaga dodatkowa: W tym widoku a
runtime_error
może być postrzegane jako coś, co przy takim samym wejściu do funkcji może teoretycznie odnieść sukces dla różnych przebiegów. (np. operacja na pliku, dostęp do bazy danych itp.)Dalsza uwaga dodatkowa: biblioteka wyrażeń regularnych w C ++ zdecydowała się wyprowadzić swój błąd,
runtime_error
chociaż istnieją przypadki, w których można go sklasyfikować tak samo jak tutaj (niepoprawny wzór wyrażeń regularnych).To po prostu pokazuje, IMHO, że grupowanie w
logic_
lubruntime_
błąd jest dość rozmyte w C ++ i tak naprawdę niewiele pomaga w ogólnym przypadku (*) - jeśli potrzebujesz poradzić sobie z konkretnymi błędami, prawdopodobnie musisz złapać mniej niż dwa.(*): To nie znaczy, że jeden kawałek kodu nie powinno być spójne, ale czy rzucać
runtime_
lublogic_
czycustom_
coś jest naprawdę nie jest aż tak ważne, jak sądzę.Aby komentować zarówno
stof
abitset
:Obie funkcje traktują ciągi jako argument, aw obu przypadkach są to:
To oświadczenie ma, IMHO, dwa korzenie:
Wydajność : jeśli funkcja jest wywoływana na ścieżce krytycznej, a przypadek „wyjątkowy” nie jest wyjątkowy, tj. Znaczna liczba przejść będzie wymagała zgłoszenia wyjątku, to płacenie za maszynę odwijania wyjątku za każdym razem nie ma sensu i może być zbyt wolny.
Miejscowość obsługi błędów : Jeśli funkcja jest wywoływana i wyjątek jest natychmiast złapany i przetwarzane, to nie ma sensu rzucać wyjątek, ponieważ obsługa błędów będzie więcej komunikatów z
catch
niż ze związkiemif
.Przykład:
Oto, gdzie pojawiają się funkcje takie jak
TryParse
vsParse
.: Jedna wersja, gdy kod lokalny oczekuje, że analizowany ciąg będzie poprawny, jedna wersja, gdy kod lokalny zakłada, że oczekiwane (tj. Nie wyjątkowe), że parsowanie zakończy się niepowodzeniem.Rzeczywiście,
stof
jest to po prostu (zdefiniowane jako) opakowaniestrtof
, więc jeśli nie chcesz wyjątków, użyj tego.IMHO, masz dwie sprawy:
Funkcja podobna do „biblioteki” (często używana w różnych kontekstach): Zasadniczo nie możesz się zdecydować. Możliwe, że dostarczymy obie wersje, być może taką, która zgłasza błąd, i opakowanie, które konwertuje zwrócony błąd na wyjątek.
Funkcja „aplikacji” (specyficzna dla obiektu blob kodu aplikacji, może być ponownie wykorzystana, ale jest ograniczona stylem obsługi błędów aplikacji itp.): Tutaj często powinna być dość wyraźna. Jeśli ścieżki kodu wywołujące funkcje obsługują wyjątki w rozsądny i użyteczny sposób, użyj wyjątków, aby zgłosić dowolny (ale patrz poniżej) błąd. Jeśli kod aplikacji jest łatwiejszy do odczytania i zapisania dla stylu zwracania błędów, należy go użyć.
Oczywiście będą między nimi miejsca - po prostu skorzystaj z potrzeb i pamiętaj o YAGNI.
Wreszcie, myślę, że powinienem wrócić do oświadczenia FAQ,
Zgadzam się na wszystkie błędy, które wyraźnie wskazują, że coś jest poważnie pomieszane lub że kod wywołujący wyraźnie nie wiedział, co robi.
Ale gdy jest to właściwe, często jest wysoce specyficzne dla aplikacji, dlatego patrz powyżej domena biblioteki vs. domena aplikacji.
To sprowadza się do pytania o to, czy i jak zweryfikować warunki wstępne wywołania , ale nie będę wchodził w to, odpowiedź jest już za długa :-)
źródło