Idiomatyczne użycie wyjątków w C ++

16

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.

cooky451
źródło
6
Musisz bardzo uważnie przeczytać ten wpis w FAQ . Odnosi się to tylko do błędów kodowania, a nie do nieprawidłowych danych, dereferencji obiektu zerowego lub czegokolwiek, co ma związek z ogólną wadliwością środowiska wykonawczego. Zasadniczo twierdzenia dotyczą identyfikacji rzeczy, które nigdy nie powinny się zdarzyć. Poza tym istnieją wyjątki, kody błędów i tak dalej.
Robert Harvey,
1
@RobertHarvey w tej definicji nadal występuje ten sam problem - jeśli coś można rozwiązać bez interwencji człowieka, lub nie jest to znane tylko wyższym warstwom programu.
cooky451
1
Wiesz się o legalizm. Oceń zalety i wady i zdecyduj. Ostatni akapit pytania ... Nie wydaje mi się, żeby to było oczywiste. Twoje myślenie jest bardzo czarno-białe, kiedy prawda jest prawdopodobnie bliższa kilku odcieniom szarości.
Robert Harvey
4
Czy próbowałeś przeprowadzić jakieś badania przed zadaniem tego pytania? Idiomy obsługi błędów C ++ są prawie na pewno omówione w mdłych szczegółach w Internecie. Jedno odniesienie do jednego wpisu FAQ nie jest dobrym badaniem. Po przeprowadzeniu badań nadal będziesz musiał podjąć decyzję. Nie zaczynaj od tego, jak nasze szkoły programistyczne najwyraźniej tworzą bezmyślne roboty kodujące wzorce oprogramowania, które nie wiedzą, jak myśleć samodzielnie.
Robert Harvey
2
Co nadaje wiarygodności mojej teorii, że taka reguła może w rzeczywistości nie istnieć. Zaprosiłem kilku ludzi z The C ++ Lounge, aby sprawdzili, czy mogą odpowiedzieć na twoje pytanie, chociaż za każdym razem, gdy tam wchodzę, ich rada brzmi: „Przestań używać C ++, to usmaży ci mózg”. Więc skorzystaj z ich rad na własne ryzyko.
Robert Harvey

Odpowiedzi:

15

Po pierwsze, czuję się zobowiązany do podkreślenia tego, std::exceptiona 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ą,

Jerry Coffin
źródło
9

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.

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.

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.

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:

  1. Nigdy
  2. Wszędzie
  3. Tylko w przypadku błędów programisty
  4. Nigdy na błędach programisty
  5. Tylko podczas nietypowych (wyjątkowych) awarii

i znajdź w internecie kogoś, kto się z tobą zgadza. Musisz przyjąć styl, który Ci odpowiada.

Winston Ewert
źródło
To może warto zauważyć, że sugestia tylko wyjątkami użycia, gdy okoliczności są naprawdę wyjątkowe był szeroko promowany przez ludzi nauki o językach, gdzie wyjątki mają słabą wydajność. C ++ nie jest jednym z tych języków.
Jules
1
@Jules - teraz (wydajność) z pewnością zasługuje na własną odpowiedź, w której popierasz swoje roszczenie. Wydajność wyjątków w C ++ jest z pewnością problemem, może bardziej, może mniej niż gdzie indziej, ale stwierdzenie, że „C ++ nie jest jednym z tych języków [gdzie wyjątki mają słabą wydajność]” jest z pewnością dyskusyjne.
Martin Ba,
1
@MartinBa - w porównaniu do, powiedzmy, Java, wydajność wyjątków C ++ jest o rząd wielkości szybsza. Testy porównawcze sugerują, że wydajność zgłaszania wyjątku w górę o 1 poziom jest około 50 razy wolniejsza niż obsługa wartości zwracanej w C ++, a ponad 1000 razy wolniejsza w Javie. Porady napisane dla Java w tym przypadku nie powinny być stosowane do C ++ bez dodatkowej przemyślenia, ponieważ między nimi jest różnica rzędów wielkości większa niż rząd wielkości. Być może powinienem napisać „wyjątkowo słabą wydajność” zamiast „słabą wydajność”.
Jules
1
@Jules - dzięki za te liczby. (jakieś źródła?) I można im wierzyć, ponieważ Java (i C #) potrzeba przechwytywać ślad stosu, który z pewnością wydaje się jak to może być bardzo kosztowne. Nadal uważam, że twoja początkowa reakcja jest nieco myląca, ponieważ nawet 50-krotne spowolnienie jest dość ciężkie, szczególnie. w języku zorientowanym na wydajność, takim jak C ++.
Martin Ba
2

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 structlub union, lub w dzisiejszych czasach boost::variantlub (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ąc operator 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, jak std::stoii boost::lexical_castktó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.

Chris Beck
źródło
1
std::expectedjest wciąż odrzuconą propozycją, prawda?
Martin Ba
Masz rację, myślę, że to jeszcze nie zostało zaakceptowane. Ale istnieje kilka implementacji typu open source, i wydaje mi się, że kilka razy wprowadziłem własne. Jest to mniej skomplikowane niż robienie wariantów, ponieważ istnieją tylko dwa możliwe stany. Głównymi rozważaniami projektowymi są: jaki dokładnie interfejs chcesz, czy chcesz, aby był on zgodny z oczekiwanym <T> Andrescu, w którym obiekt błędu powinien być exception_ptr, czy po prostu chcesz użyć jakiegoś typu struktury lub czegoś tak.
Chris Beck
Wystąpienie Andrei Alexandrescu jest tutaj: channel9.msdn.com/Shows/Going+Deep/... Pokazuje szczegółowo, jak zbudować taką klasę i jakie masz względy.
Chris Beck
Zaproponowane rozwiązanie [[nodiscard]] attributebędzie pomocne w tym podejściu do obsługi błędów, ponieważ zapewnia, że ​​nie zignorujesz wyniku błędu przypadkowo.
CodesInChaos
- tak, znałem przemowę AA. Uznałem, że projekt jest dość dziwny, ponieważ aby go rozpakować ( 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.
Martin Ba
1

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 Employeeklasę 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.

vin
źródło
1

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:

przekazanie pustego ciągu do std :: stof (wyrzuci niepoprawny argument), a nie błąd programowania

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_errorbłę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_errorjest 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_errorchociaż 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_lub runtime_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_lub logic_czy custom_coś jest naprawdę nie jest aż tak ważne, jak sądzę.


Aby komentować zarówno stofa bitset:

Obie funkcje traktują ciągi jako argument, aw obu przypadkach są to:

  • nie jest trywialne sprawdzanie, czy dany ciąg znaków jest poprawny (np. w najgorszym przypadku konieczne byłoby zreplikowanie logiki funkcji; w przypadku zestawu bitów nie jest od razu jasne, czy pusty ciąg jest poprawny, więc niech ctor zdecyduje)
  • Funkcja ta jest już odpowiedzialna za „parsowanie” łańcucha, więc musi już sprawdzić poprawność łańcucha, więc ma sens, że zgłasza błąd „równomiernego użycia” łańcucha (w obu przypadkach jest to wyjątek) .

regułą, 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?

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 zcatch niż ze związkiem if.

Przykład:

float readOrDefault;
try {
  readOrDefault = stof(...);
} catch(std::exception&) {
  // discard execption, just use default value
  readOrDefault = 3.14f; // 3.14 is the default value if cannot be read
}

Oto, gdzie pojawiają się funkcje takie jak TryParsevs Parse.: 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, stofjest to po prostu (zdefiniowane jako) opakowanie strtof, więc jeśli nie chcesz wyjątków, użyj tego.


Jak więc mam zdecydować, czy mam stosować wyjątki, czy nie dla określonej funkcji?

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,

Nie używaj rzutu, aby wskazać błąd kodowania podczas używania funkcji. Użyj mechanizmu aser lub innego mechanizmu, aby wysłać proces do debugera lub go zawiesić ...

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 :-)

Martin Ba
źródło