Czy używanie funkcji assert () w C ++ jest złą praktyką?

94

Mam tendencję do dodawania wielu asercji do mojego kodu C ++, aby ułatwić debugowanie bez wpływu na wydajność kompilacji wydania. Teraz assertjest czystym makrem C, zaprojektowanym bez uwzględnienia mechanizmów C ++.

Z drugiej strony C ++ definiuje std::logic_error, który ma być wyrzucany w przypadkach, gdy występuje błąd w logice programu (stąd nazwa). Rzucanie instancji może być po prostu doskonałą, bardziej C ++ alternatywną alternatywą dla assert.

Problem polega na tym, że asserti abortoba kończą program natychmiast bez wywoływania destruktorów, pomijając w ten sposób czyszczenie, podczas gdy ręczne zgłaszanie wyjątku powoduje niepotrzebne koszty działania. Jednym ze sposobów obejścia tego byłoby utworzenie własnego makra asercji SAFE_ASSERT, które działa tak samo jak odpowiednik w C, ale zgłasza wyjątek w przypadku niepowodzenia.

Przychodzą mi na myśl trzy opinie na ten temat:

  • Trzymaj się twierdzenia C. Ponieważ program jest natychmiast przerywany, nie ma znaczenia, czy zmiany zostały poprawnie rozwinięte. Również używanie #defines w C ++ jest równie złe.
  • Wrzuć wyjątek i złap go w main () . Pozwalanie kodowi na pomijanie destruktorów w dowolnym stanie programu jest złą praktyką i należy tego unikać za wszelką cenę, podobnie jak wywołania terminate (). Jeśli rzucane są wyjątki, muszą zostać złapane.
  • Zgłoś wyjątek i pozwól mu zakończyć program. Wyjątek kończący program jest w porządku iz tego powodu NDEBUGnigdy nie nastąpi to w kompilacji wydania. Przechwytywanie jest niepotrzebne i ujawnia szczegóły implementacji kodu wewnętrznego main().

Czy istnieje ostateczna odpowiedź na ten problem? Jakieś profesjonalne referencje?

Edytowano: pomijanie destruktorów nie jest oczywiście niezdefiniowanym zachowaniem.

Fabian Knorr
źródło
22
Nie, naprawdę logic_errorjest to błąd logiczny. Błąd w logice programu nazywa się błędem. Nie rozwiązujesz błędów, rzucając wyjątki.
R. Martinho Fernandes
4
Asercje, wyjątki, kody błędów. Każdy ma zupełnie inny przypadek użycia i nie należy go używać tam, gdzie jest potrzebny inny.
Kerrek SB,
5
Upewnij się, że używasz static_asserttam, gdzie jest to właściwe, jeśli masz to dostępne.
Flexo
4
@trion Nie wiem, jak to pomaga. Rzuciłbyś std::bug?
R. Martinho Fernandes
3
@trion: Nie rób tego. Wyjątki nie służą do debugowania. Ktoś może łapać wyjątek. Nie musisz martwić się o UB podczas dzwonienia std::abort(); po prostu podniesie sygnał, który spowoduje zakończenie procesu.
Kerrek SB,

Odpowiedzi:

74

Asercje są całkowicie odpowiednie w kodzie C ++. Wyjątki i inne mechanizmy obsługi błędów nie są tak naprawdę przeznaczone do tego samego, co asercje.

Obsługa błędów ma zastosowanie, gdy istnieje możliwość odzyskania lub zgrabnego zgłoszenia błędu użytkownikowi. Na przykład, jeśli wystąpi błąd podczas próby odczytania pliku wejściowego, możesz chcieć coś z tym zrobić. Błędy mogą wynikać z błędów, ale mogą też być po prostu odpowiednim wyjściem dla danego wejścia.

Asercje służą do takich rzeczy, jak sprawdzanie, czy wymagania API są spełnione, gdy normalnie API nie byłoby sprawdzane, lub do sprawdzania rzeczy, które deweloper uważa, że ​​gwarantuje to konstrukcja. Na przykład, jeśli algorytm wymaga posortowanych danych wejściowych, normalnie nie sprawdzałbyś tego, ale możesz mieć asercję, aby to sprawdzić, aby debugowanie kompilowało flagę tego rodzaju błędu. Stwierdzenie powinno zawsze wskazywać na nieprawidłowo działający program.


Jeśli piszesz program, w którym nieczyste zamknięcie może spowodować problem, możesz chcieć uniknąć asercji. Niezdefiniowane zachowanie ściśle w zakresie języka C ++ nie kwalifikuje się tutaj jako taki problem, ponieważ trafienie w asercję jest prawdopodobnie już wynikiem niezdefiniowanego zachowania lub naruszenia jakiegoś innego wymagania, które mogłoby uniemożliwić poprawne działanie niektórych porządków.

Również jeśli zaimplementujesz asercje w kategoriach wyjątku, może to potencjalnie zostać przechwycone i „obsłużone”, nawet jeśli jest to sprzeczne z samym celem stwierdzenia.

bames53
źródło
1
Nie jestem do końca pewien, czy zostało to określone konkretnie w odpowiedzi, więc powiem to tutaj: nie należy używać potwierdzenia do niczego, co dotyczy danych wejściowych użytkownika, czego nie można określić w momencie pisania kodu. Jeśli użytkownik przejdzie 3zamiast 1do twojego kodu, generalnie nie powinno to wyzwalać asercji. Asercje są tylko błędem programisty, a nie błędem użytkownika biblioteki lub aplikacji.
SS Anne
101
  • Asercje służą do debugowania . Użytkownik wysłanego kodu nigdy nie powinien ich widzieć. Jeśli dojdzie do asercji, Twój kod musi zostać naprawiony.

    CWE-617: Reachable Assertion

Produkt zawiera assert () lub podobne stwierdzenie, które może zostać wywołane przez osobę atakującą, co prowadzi do zamknięcia aplikacji lub innego zachowania, które jest poważniejsze niż to konieczne.

Chociaż asercja jest dobra do wychwytywania błędów logicznych i zmniejszania szans na osiągnięcie poważniejszych warunków podatności, nadal może prowadzić do odmowy usługi.

Na przykład, jeśli serwer obsługuje wiele jednoczesnych połączeń, a funkcja assert () występuje w jednym połączeniu, co powoduje porzucenie wszystkich innych połączeń, jest to stwierdzenie osiągalne, które prowadzi do odmowy usługi.

  • Wyjątki dotyczą wyjątkowych okoliczności . Jeśli zostanie napotkany, użytkownik nie będzie mógł robić tego, co chce, ale może wznowić działanie w innym miejscu.

  • Obsługa błędów dotyczy normalnego przebiegu programu. Na przykład, jeśli poprosisz użytkownika o numer i otrzymasz coś, czego nie można przeanalizować, jest to normalne , ponieważ dane wejściowe użytkownika nie są pod twoją kontrolą i zawsze musisz zawsze radzić sobie ze wszystkimi możliwymi sytuacjami. (Np. Zapętlaj, aż uzyskasz prawidłowe dane wejściowe, mówiąc „Przepraszamy, spróbuj ponownie” w międzyczasie).

Kerrek SB
źródło
1
przyszedł szukać tego ponownego potwierdzenia; jakakolwiek forma potwierdzenia przechodząca do kodu produkcyjnego wskazuje na zły projekt i kontrolę jakości. Punkt, w którym wywoływana jest asercja, jest miejscem, w którym powinna przebiegać wdzięczna obsługa warunku błędu. (Nigdy nie używam assertów). Jeśli chodzi o wyjątki, jedyny znany mi przypadek użycia to sytuacja, w której ctor może zawieść, a wszystkie inne służą do normalnej obsługi błędów.
slashmais
5
@slashmais: Ten sentyment jest godny pochwały, ale jeśli nie wysyłasz idealnego, wolnego od błędów kodu, uważam, że twierdzenie (nawet takie, które powoduje awarię użytkownika) jest lepsze niż niezdefiniowane zachowanie. Błędy zdarzają się w złożonych systemach, a dzięki zapewnieniu masz sposób, aby je zobaczyć i zdiagnozować, gdzie to się dzieje.
Kerrek SB
@KerrekSB Wolałbym użyć wyjątku zamiast potwierdzenia. Przynajmniej kod ma szansę odrzucić wadliwą gałąź i zrobić coś innego pożytecznego. Przynajmniej, jeśli używasz RAII, wszystkie twoje bufory do otwierania plików zostaną poprawnie opróżnione.
daemonspring
14

Asercje mogą być używane do weryfikacji wewnętrznych niezmienników implementacji, takich jak stan wewnętrzny przed lub po wykonaniu jakiejś metody, itp. Jeśli asercja nie powiedzie się, oznacza to, że logika programu jest zepsuta i nie można tego naprawić. W takim przypadku najlepsze, co możesz zrobić, to jak najszybciej się zepsuć bez przekazywania wyjątku użytkownikowi. Naprawdę fajne w asercjach (przynajmniej w Linuksie) jest to, że zrzut pamięci jest generowany w wyniku zakończenia procesu, a zatem można łatwo zbadać ślad stosu i zmienne. Jest to znacznie bardziej przydatne do zrozumienia błędu logiki niż komunikatu o wyjątku.

nogard
źródło
Mam podobne podejście. Do logiki używam asercji, które powinny być prawdopodobnie poprawne lokalnie (np. Niezmienniki pętli). Wyjątki dotyczą sytuacji, w których błąd logiczny został wymuszony w kodzie przez sytuację nielokalną (zewnętrzną).
spraff
Jeśli asercja nie powiedzie się, oznacza to, że logika części programu jest zepsuta. Nieudane stwierdzenie niekoniecznie oznacza, że nic nie można osiągnąć. Zepsuta wtyczka prawdopodobnie nie powinna przerywać pracy całego edytora tekstu.
daemonspring
13

Brak działania destruktorów z powodu allingu abort () nie jest niezdefiniowanym zachowaniem!

Gdyby tak było, wywołanie std::terminate()również byłoby niezdefiniowanym zachowaniem , a więc jaki byłby sens w ich zapewnianiu?

assert() jest tak samo przydatna w C ++, jak w C. Asercje nie służą do obsługi błędów, służą do natychmiastowego przerywania programu.

Jonathan Wakely
źródło
1
Powiedziałbym, że abort()za natychmiastowe przerwanie programu. Masz rację, że asercje nie służą do obsługi błędów, ale assert próbuje obsłużyć błąd przez przerwanie. Czy nie powinieneś zamiast tego zgłosić wyjątku i pozwolić wywołującemu obsłużyć błąd, jeśli to możliwe? W końcu wywołujący jest w lepszej pozycji, aby określić, czy awaria jednej funkcji sprawia, że ​​nie warto robić nic innego. Być może dzwoniący próbuje wykonać trzy niezwiązane ze sobą rzeczy i nadal może ukończyć pozostałe dwa zadania i po prostu odrzucić to.
daemonspring
I assertjest zdefiniowany do wywołania abort(gdy warunek jest fałszywy). Jeśli chodzi o rzucanie wyjątków, nie, nie zawsze jest to właściwe. Niektóre rzeczy nie mogą być obsługiwane przez dzwoniącego. Wzywający nie może określić, czy błąd logiczny w funkcji biblioteki innej firmy można odzyskać lub czy można naprawić uszkodzone dane.
Jonathan Wakely
6

IMHO, asercje służą do sprawdzania warunków, które jeśli zostaną naruszone, sprawią, że wszystko inne będzie bezsensowne. I dlatego nie możesz po nich wyzdrowieć, a raczej odzyskiwanie jest nieistotne.

Pogrupowałbym je w 2 kategorie:

  • Grzechy dewelopera (np. Funkcja prawdopodobieństwa, która zwraca wartości ujemne):

prawdopodobieństwo float () {return -1.0; }

assert (prawdopodobieństwo ()> = 0,0)

  • Maszyna jest zepsuta (np. Maszyna, na której działa twój program, jest bardzo zła):

int x = 1;

assert (x> 0);

Są to trywialne przykłady, ale niezbyt odległe od rzeczywistości. Na przykład pomyśl o naiwnych algorytmach, które zwracają ujemne indeksy do użycia z wektorami. Lub programy osadzone w niestandardowym sprzęcie. A raczej dlatego, że dzieje się gówno .

A jeśli wystąpią takie błędy programistyczne, nie powinieneś mieć pewności co do zaimplementowanego mechanizmu odzyskiwania lub obsługi błędów. To samo dotyczy błędów sprzętowych.

FranMowinckel
źródło
1
assert (prawdopodobieństwo ()> = 0,0)
Elliott