Wydaje się, że C ++ woli częściej używać wyjątków.
Pod pewnymi względami zasugerowałbym mniej niż Objective-C, ponieważ standardowa biblioteka C ++ nie generowałaby generalnie błędów programistycznych, takich jak brak dostępu do sekwencji losowego dostępu w jej najczęstszej postaci ( operator[]
np.) Lub próba wyłuskiwania nieprawidłowego iteratora. Język nie rzuca się na dostęp do tablicy poza granicami, dereferencję wskaźnika zerowego lub coś w tym rodzaju.
Usunięcie błędów programisty w dużej mierze z równania obsługi wyjątków faktycznie usuwa bardzo dużą kategorię błędów, na które często reagują inne języki throwing
. C ++ ma tendencję do assert
(która nie kompiluje się w kompilacjach wydania / produkcji, tylko kompilacje debugowania) lub po prostu wyładowuje (często ulega awarii) w takich przypadkach, prawdopodobnie częściowo dlatego, że język nie chce nakładać kosztów takich kontroli środowiska wykonawczego co byłoby wymagane do wykrycia takich błędów programisty, chyba że programista specjalnie chce ponieść koszty, pisząc kod, który sam dokonuje takich kontroli.
Sutter zachęca nawet do unikania wyjątków w takich przypadkach w standardach kodowania C ++:
Podstawową wadą używania wyjątku do zgłaszania błędu programowania jest to, że tak naprawdę nie chcesz, aby odwijanie stosu miało miejsce, gdy chcesz, aby debuger uruchomił się dokładnie w wierszu, w którym wykryto naruszenie, z nienaruszonym stanem linii. Podsumowując: istnieją błędy, o których wiesz, że mogą się zdarzyć (patrz pozycje 69–75). Jeśli chodzi o wszystko inne, co nie powinno, a jest to wina programisty, jeśli tak, jest assert
.
Ta zasada niekoniecznie musi być ugruntowana. W niektórych bardziej krytycznych przypadkach może być wskazane użycie opakowania, powiedzmy, opakowania i standardu kodowania, który jednolicie rejestruje, gdzie występują błędy programisty i throw
w przypadku błędów programisty, takich jak próba uszanowania czegoś nieprawidłowego lub uzyskania dostępu poza granicami, ponieważ w tych przypadkach odzyskanie oprogramowania może być zbyt kosztowne, jeśli oprogramowanie ma na to szansę. Ale ogólnie rzecz biorąc, bardziej powszechne użycie języka sprzyja nie rzucaniu się w obliczu błędów programisty.
Wyjątki zewnętrzne
Tam, gdzie widzę wyjątki najczęściej wspierane w C ++ (zgodnie ze standardowym komitetem, np.) Dotyczy „wyjątków zewnętrznych”, ponieważ w nieoczekiwany sposób niektóre zewnętrzne źródła poza programem. Przykładem jest nieprzydzielenie pamięci. Innym jest to, że nie można otworzyć pliku krytycznego wymaganego do uruchomienia oprogramowania. Innym nie udało się połączyć z wymaganym serwerem. Innym jest użytkownik, który zacina przycisk przerwania, aby anulować operację, której wspólna ścieżka wykonywania spraw oczekuje, że zakończy się ona sukcesem bez tej zewnętrznej przerwy. Wszystkie te rzeczy są poza kontrolą bezpośredniego oprogramowania i programistów, którzy je napisali. Są to nieoczekiwane wyniki ze źródeł zewnętrznych, które uniemożliwiają operację (którą tak naprawdę należy uważać za niepodzielną transakcję w mojej książce *).
Transakcje
Często zachęcam do patrzenia na try
blok jako „transakcję”, ponieważ transakcje powinny odnieść sukces jako całość lub zawieść jako całość. Jeśli próbujemy coś zrobić, a to nie powiedzie się w połowie, wszelkie efekty uboczne / mutacje wprowadzone do stanu programu zazwyczaj muszą zostać wycofane, aby przywrócić system do prawidłowego stanu, tak jakby transakcja nigdy nie została wykonana, podobnie jak RDBMS, który nie przetwarza zapytania w połowie, nie powinien naruszać integralności bazy danych. Jeśli mutujesz stan programu bezpośrednio we wspomnianej transakcji, musisz „cofnąć mutację” po napotkaniu błędu (i tutaj osłony zakresu mogą być przydatne w RAII).
Znacznie prostszą alternatywą jest nie mutowanie stanu oryginalnego programu; możesz zmutować kopię, a następnie, jeśli się powiedzie, zamień kopię na oryginał (upewniając się, że zamiana nie będzie możliwa). Jeśli się nie powiedzie, odrzuć kopię. Dotyczy to również sytuacji, gdy nie używasz wyjątków do obsługi błędów w ogóle. „Transakcyjny” sposób myślenia jest kluczem do prawidłowego powrotu do zdrowia, jeśli przed wystąpieniem błędu wystąpiły mutacje stanu programu. Albo odnosi sukces jako całość, albo porażka jako całość. Nie jest w połowie w stanie dokonać mutacji.
Jest to dziwnie jeden z najrzadziej omawianych tematów, gdy widzę, że programiści pytają, jak poprawnie obsługiwać błędy lub wyjątki, ale najtrudniej jest uzyskać wszystko w każdym oprogramowaniu, które chce bezpośrednio mutować stan programu w wielu jego operacje. Czystość i niezmienność mogą tutaj pomóc w osiągnięciu bezpieczeństwa wyjątkowego w takim samym stopniu, jak pomagają w zabezpieczeniu nici, ponieważ mutacja / zewnętrzny efekt uboczny, który nie występuje, nie musi być cofany.
Wydajność
Kolejnym czynnikiem decydującym o tym, czy stosować wyjątki, jest wydajność, i nie mam na myśli obsesyjnego, szczypiącego grosza, odwrotnego do zamierzonego efektu. Wiele kompilatorów C ++ implementuje tak zwaną „obsługę wyjątków zerowych kosztów”.
Zapewnia zerowe obciążenie środowiska wykonawczego dla wykonania bezbłędnego, co przewyższa nawet obsługę błędów zwracanych przez wartość C. Jako kompromis propagacja wyjątku ma duże koszty ogólne.
Zgodnie z tym, co o nim czytałem, sprawia, że ścieżki wykonywania zwykłych spraw nie wymagają narzutu (nawet narzutu, który zwykle towarzyszy obsłudze i propagacji kodów błędów w stylu C), w zamian za znaczne przesunięcie kosztów w kierunku wyjątkowych ścieżek ( co oznacza, że throwing
jest teraz droższy niż kiedykolwiek).
„Drogie” jest nieco trudne do oszacowania, ale na początek prawdopodobnie nie chcesz rzucać miliona razy w jakąś ciasną pętlę. Ten rodzaj projektu zakłada, że wyjątki nie występują przez cały czas po lewej i po prawej stronie.
Brak błędów
I ten punkt wydajności prowadzi mnie do braku błędów, co jest zaskakująco niewyraźne, jeśli spojrzymy na wiele innych języków. Ale powiedziałbym, biorąc pod uwagę wspomniany powyżej projekt EH o zerowym koszcie, że prawie na pewno nie chcesz tego throw
w odpowiedzi na brak klucza w zestawie. Ponieważ nie tylko jest to prawdopodobnie błąd (osoba szukająca klucza mogła zbudować zestaw i oczekiwać, że będzie szukała kluczy, które nie zawsze istnieją), ale w tym kontekście byłaby niezwykle droga.
Na przykład funkcja przecinania zestawu może chcieć przejrzeć dwa zestawy i wyszukać klucze, które mają wspólne. Jeśli nie uda ci się znaleźć klucza threw
, będziesz zapętlał się i możesz napotkać wyjątki w połowie lub więcej iteracji:
Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
Set<int> intersection;
for (int key: a)
{
try
{
b.find(key);
intersection.insert(other_key);
}
catch (const KeyNotFoundException&)
{
// Do nothing.
}
}
return intersection;
}
Ten powyższy przykład jest absolutnie śmieszny i przesadzony, ale widziałem w kodzie produkcyjnym, że niektórzy ludzie pochodzący z innych języków używają wyjątków w C ++ w podobny sposób, i myślę, że jest to dość praktyczne stwierdzenie, że nie jest to właściwe użycie wyjątków w C ++. Inną wskazówką powyżej jest to, że zauważysz, że catch
blok nie ma absolutnie nic do roboty i jest napisany, aby zignorować takie wyjątki, i zwykle jest to wskazówka (choć nie jest gwarantem), że wyjątki prawdopodobnie nie są używane odpowiednio w C ++.
W przypadku tego rodzaju przypadków pewien rodzaj wartości zwracanej wskazującej błąd (wszystko od powrotu false
do nieprawidłowego iteratora lub nullptr
cokolwiek innego, co ma sens w kontekście) jest zwykle o wiele bardziej odpowiedni, a także często bardziej praktyczny i produktywny, ponieważ nie zawiera błędów case zwykle nie wymaga jakiegoś procesu rozwijania stosu, aby dotrzeć do analogicznej catch
strony.
pytania
Musiałbym użyć wewnętrznych flag błędów, jeśli zdecyduję się uniknąć wyjątków. Czy będzie to zbyt kłopotliwe w obsłudze, czy może będzie działało nawet lepiej niż wyjątki? Najlepszym rozwiązaniem byłoby porównanie obu przypadków.
Unikanie wyjątków wprost w C ++ wydaje mi się wyjątkowo przeciwne do zamierzonego, chyba że pracujesz w jakimś systemie wbudowanym lub w konkretnym rodzaju skrzynki, która zabrania ich używania (w takim przypadku musisz również zrobić wszystko, aby uniknąć wszystkich biblioteka i funkcjonalność językowa, która w innym przypadku byłaby throw
ściśle używana nothrow
new
).
Jeśli absolutnie musisz unikać wyjątków z jakiegokolwiek powodu (np. Praca nad granicami C API modułu, którego eksportujesz C API), wielu może się ze mną nie zgodzić, ale w rzeczywistości sugerowałbym użycie globalnego modułu obsługi błędów / statusu, takiego jak OpenGL glGetError()
. Możesz użyć pamięci lokalnej, aby mieć unikalny status błędu dla każdego wątku.
Uzasadniam to tym, że nie jestem przyzwyczajony do patrzenia na zespoły w środowisku produkcyjnym, dokładnie sprawdzające wszystkie możliwe błędy, niestety, gdy zwracane są kody błędów. Gdyby były dokładne, niektóre interfejsy API C mogą napotkać błąd przy prawie każdym pojedynczym wywołaniu API C, a dokładne sprawdzenie wymagałoby:
if ((err = ApiCall(...)) != success)
{
// Handle error
}
... przy prawie każdym wierszu kodu wywołującym interfejs API wymagającym takich kontroli. Jednak nie miałem szczęścia pracować z tak dokładnymi zespołami. Często ignorują takie błędy w połowie, a czasem nawet w większości przypadków. To dla mnie największy apel wyjątków. Jeśli opakowujemy ten interfejs API i ujednolicamy go throw
po napotkaniu błędu, wyjątku nie można zignorować , a moim zdaniem i doświadczenia, właśnie tam leży przewaga wyjątków.
Ale jeśli nie można zastosować wyjątków, to globalny status błędu według wątków ma przynajmniej tę zaletę (ogromną w porównaniu do zwracania mi kodów błędów), że może mieć szansę złapać poprzedni błąd nieco później niż wtedy, gdy wystąpił w jakiejś niechlujnej bazie kodu, zamiast go całkowicie pominąć i pozostawiając nas całkowicie nieświadomymi co się stało. Błąd mógł pojawić się kilka linii wcześniej lub w poprzednim wywołaniu funkcji, ale pod warunkiem, że oprogramowanie jeszcze się nie zawiesiło, możemy zacząć pracować wstecz i dowiedzieć się, gdzie i dlaczego.
Wydaje mi się, że ponieważ wskaźniki są rzadkie, musiałbym stosować wewnętrzne flagi błędów, jeśli zdecyduję się uniknąć wyjątków.
Niekoniecznie powiedziałbym, że wskaźniki są rzadkie. Istnieją nawet metody w C ++ 11 i nowsze, aby uzyskać dostęp do bazowych wskaźników danych kontenerów i nowego nullptr
słowa kluczowego. Generalnie uważa się za nierozsądne stosowanie surowych wskaźników do posiadania / zarządzania pamięcią, jeśli można użyć czegoś podobnego, unique_ptr
biorąc pod uwagę, jak krytyczne jest to, aby być zgodnym z RAII w przypadku wyjątków. Jednak surowe wskaźniki, które nie posiadają pamięci / nie zarządzają pamięcią, niekoniecznie są uważane za tak złe (nawet od ludzi takich jak Sutter i Stroustrup), a czasem bardzo praktyczne jako sposób wskazywania na rzeczy (wraz ze wskaźnikami wskazującymi na rzeczy).
Są one prawdopodobnie nie mniej bezpieczne niż standardowe iteratory kontenerów (przynajmniej w wersji, nieobecne sprawdzone iteratory), które nie wykryją, jeśli spróbujesz je wyrenderować po ich unieważnieniu. Powiedziałbym, że C ++ wciąż jest trochę niebezpiecznym językiem, chyba że twoje specyficzne użycie chce opakować wszystko i ukryć nawet nieposiadające surowych wskaźników. Jest prawie krytyczne z wyjątkami, że zasoby są zgodne z RAII (co generalnie nie wiąże się z żadnymi kosztami środowiska uruchomieniowego), ale poza tym niekoniecznie stara się być najbezpieczniejszym językiem w celu uniknięcia kosztów, których deweloper wyraźnie nie chce wymienić na coś innego. Zalecane użycie nie próbuje chronić cię przed takimi zwisającymi wskaźnikami i unieważnionymi iteratorami, że tak powiem (w przeciwnym razie zachęcamy do korzystaniashared_ptr
w całym miejscu, któremu Stroustrup gwałtownie się sprzeciwia). Stara się chronić cię przed niepowodzeniem w prawidłowym uwolnieniu / zwolnieniu / zniszczeniu / odblokowaniu / oczyszczeniu zasobu, gdy coś throws
.