W funkcji, która przyjmuje parametry, osoba wywołująca może być w stanie przekazać argumenty, które są poprawne składniowo, ale nie mają znaczenia semantycznego. Na przykład w poprzedniej lekcji (9.4 — Wykrywanie i obsługa błędów) pokazaliśmy następującą przykładową funkcję:
void printDivision(int x, int y)
{
if (y != 0)
std::cout << static_cast<double>(x) / y;
else
std::cerr << "Error: Could not divide by zero\n";
}Ta funkcja jawnie sprawdza, czy y Jest 0, ponieważ dzielenie przez zero jest błędem semantycznym i spowoduje awarię programu w przypadku wykonania.
W poprzedniej lekcji omówiliśmy kilka sposobów radzenia sobie z takimi problemami, w tym wstrzymanie programu lub pominięcie obraźliwych stwierdzeń.
Obie te opcje są jednak problematyczne. Jeśli program pominie instrukcje z powodu błędu, oznacza to w zasadzie ciche niepowodzenie. Zwłaszcza gdy piszemy i debugujemy programy, ciche awarie są złe, ponieważ przesłaniają prawdziwe problemy. Nawet jeśli wydrukujemy komunikat o błędzie, może on zostać utracony wśród wyników innego programu i może nie być oczywiste, gdzie jest generowany komunikat o błędzie lub w jaki sposób wystąpiły warunki, które wyzwoliły komunikat o błędzie. Niektóre funkcje mogą być wywoływane dziesiątki lub setki razy, a jeśli tylko jeden z tych przypadków generuje problem, może być trudno ustalić, który.
Jeśli program zakończy działanie (przez std::exit), utracimy stos wywołań i wszelkie informacje debugowania, które mogą pomóc nam w wyizolowaniu problemu. std::abort jest lepszą opcją w takich przypadkach, ponieważ zazwyczaj programista będzie miał możliwość rozpoczęcia debugowania w miejscu, w którym program przerwane.
Warunki wstępne, niezmienniki i warunki końcowe
W programowaniu warunek wstępny to dowolny warunek, który musi być spełniony przed wykonaniem jakiejś sekcji kodu (zazwyczaj treści funkcji). W poprzednim przykładzie sprawdziliśmy, czy y != 0 jest warunkiem wstępnym zapewniającym, że y ma wartość różną od zera przed podzieleniem przez y.
Warunki wstępne dla funkcji najlepiej umieścić na górze funkcji, korzystając z wczesnego powrotu, aby wrócić do osoby wywołującej, jeśli warunek wstępny nie jest spełniony. Na przykład:
void printDivision(int x, int y)
{
if (y == 0) // handle
{
std::cerr << "Error: Could not divide by zero\n";
return; // bounce the user back to the caller
}
// We now know that y != 0
std::cout << static_cast<double>(x) / y;
}Odczyt opcjonalny
Jest to czasami nazywane „wzorcem odbijacza”, ponieważ po wykryciu błędu następuje natychmiastowe wyrzucenie z funkcji.
Wzorzec odbijania ma dwie główne zalety:
- Wszystkie przypadki testowe są od początku udostępniane, a przypadek testowy i kod obsługujący błąd są razem.
- Otrzymujesz mniej zagnieżdżanie.
Oto jak wygląda wersja bez odbijania:
void printDivision(int x, int y)
{
if (y != 0)
{
std::cout << static_cast<double>(x) / y;
}
else
{
std::cerr << "Error: Could not divide by zero\n";
return; // bounce the user back to the caller
}
}Ta wersja jest znacznie gorsza, ponieważ przypadek testowy i kod obsługujący błąd są bardziej oddzielone i występuje więcej zagnieżdżeń.
An Niezmiennik to warunek, który musi być spełniony podczas wykonywania jakiejś sekcji kodu. Jest to często używane w pętlach, gdzie treść pętli będzie wykonywana tylko tak długo, jak niezmiennik będzie prawdziwy.
Dla zaawansowanych czytelników
Mówimy o powszechnym typie niezmiennika zwanym „niezmiennikiem klasy” w lekcji >14.2 -- Wprowadzenie do klas.
Podobnie warunek końcowy jest czymś, co musi być prawdziwe po wykonaniu jakiejś części kodu. Nasza funkcja nie ma żadnych warunków końcowych.
Asercje
Użycie instrukcji warunkowej do wykrycia nieprawidłowego parametru (lub sprawdzenia innego założenia) wraz z wyświetleniem komunikatu o błędzie i zakończeniem programu jest tak powszechną metodą wykrywania problemów, że C++ udostępnia do tego skrótową metodę.
An Asercja jest wyrażeniem, które będzie prawdziwe, chyba że w programie jest błąd. Jeśli wyrażenie ma wartość true, instrukcja asercji nie robi nic. Jeżeli wyrażenie warunkowe ma wartość false, wyświetlany jest komunikat o błędzie i program zostaje zakończony (przez std::abort). Ten komunikat o błędzie zazwyczaj zawiera wyrażenie, które zakończyło się niepowodzeniem, jako tekst, wraz z nazwą pliku kodu i numerem wiersza potwierdzenia. Dzięki temu bardzo łatwo jest stwierdzić nie tylko, na czym polegał problem, ale także gdzie w kodzie wystąpił problem. Może to ogromnie pomóc w debugowaniu.
Kluczowa informacja
Asertacje służą do wykrywania błędów podczas programowania i debugowania.
Gdy asercja ma wartość false, program jest natychmiast zatrzymywany. Daje to możliwość użycia narzędzi do debugowania w celu sprawdzenia stanu programu i ustalenia, dlaczego asercja nie powiodła się. Działając wstecz, możesz szybko znaleźć i naprawić problem.
Bez potwierdzenia wykrycia błędu i niepowodzenia, taki błąd prawdopodobnie spowodowałby późniejsze nieprawidłowe działanie programu. W takich przypadkach określenie, gdzie coś idzie nie tak lub jaka jest pierwotna przyczyna problemu, może być bardzo trudne.
W C++ asercje w czasie wykonywania są implementowane poprzez makro preprocesoraassert , które znajduje się w nagłówku <cassert>.
#include <cassert> // for assert()
#include <cmath> // for std::sqrt
#include <iostream>
double calculateTimeUntilObjectHitsGround(double initialHeight, double gravity)
{
assert(gravity > 0.0); // The object won't reach the ground unless there is positive gravity.
if (initialHeight <= 0.0)
{
// The object is already on the ground. Or buried.
return 0.0;
}
return std::sqrt((2.0 * initialHeight) / gravity);
}
int main()
{
std::cout << "Took " << calculateTimeUntilObjectHitsGround(100.0, -9.8) << " second(s)\n";
return 0;
}Gdy program wywoła calculateTimeUntilObjectHitsGround(100.0, -9.8), assert(gravity > 0.0) otrzyma false, co spowoduje wyzwolenie potwierdzenia. Spowoduje to wydrukowanie komunikatu podobnego do tego:
dropsimulator: src/main.cpp:6: double calculateTimeUntilObjectHitsGround(double, double): Assertion 'gravity > 0.0' failed.
Rzeczywisty komunikat różni się w zależności od używanego kompilatora.
Chociaż asercje są najczęściej używane do sprawdzania parametrów funkcji, można ich używać wszędzie tam, gdzie chcesz sprawdzić, czy coś jest prawdziwe.
Chociaż już wcześniej powiedzieliśmy, abyś unikał makr preprocesora, asercje są jednymi z niewielu makr preprocesora, których użycie jest dopuszczalne. Zachęcamy do swobodnego stosowania instrukcji potwierdzających w całym kodzie.
Kluczowa informacja
Twierdzenia są lepsze niż komentarze, ponieważ zawierają zarówno dokument, jak i wymuszają warunek. Komentarze mogą stać się nieaktualne, gdy kod ulegnie zmianie, a komentarz nie zostanie zaktualizowany. Asercja, która stała się nieaktualna, jest problemem z poprawnością kodu, więc programiści rzadziej pozwalają na jej marnowanie.
Nadanie wypowiedziom asertywności bardziej opisowego
Czasami wyrażenia asercji nie są zbyt opisowe. Rozważmy następujące stwierdzenie:
assert(found);Jeśli to twierdzenie zostanie wywołane, twierdzenie powie:
Assertion failed: found, file C:\\VCProjects\\Test.cpp, line 34
Co to w ogóle oznacza? Najwyraźniej found było false (od momentu uruchomienia asercji), ale czego nie znaleziono? Aby to stwierdzić, musiałbyś zajrzeć do kodu.
Na szczęście istnieje mały trik, dzięki któremu Twoje stwierdzenia staną się bardziej opisowe. Po prostu dodaj literał łańcuchowy połączony logicznym AND:
assert(found && "Car could not be found in database");Oto dlaczego to działa: Literał łańcuchowy zawsze ma wartość Boolean true. Zatem jeśli found Jest false, false && true Jest false. Jeżeli found Jest true, true && true Jest true. Zatem logiczne ORAZ literału łańcuchowego nie ma wpływu na ocenę potwierdzenia.
Jednak gdy aktywuje się potwierdzenie, literał ciągu zostanie uwzględniony w komunikacie potwierdzenia:
Assertion failed: found && "Car could not be found in database", file C:\\VCProjects\\Test.cpp, line 34
Daje to dodatkowy kontekst tego, co poszło nie tak.
Używanie asercji w przypadku niezaimplementowanych funkcji
Twierdzenia są czasami używane do dokumentowania przypadków, które nie zostały zaimplementowane, ponieważ nie były potrzebne w momencie pisania kodu przez programistę:
assert(moved && "Need to handle case where student was just moved to another classroom");W ten sposób, jeśli programista napotka sytuację, w której potrzebny jest ten przypadek, kod zakończy się niepowodzeniem i wyświetli się przydatny komunikat o błędzie, a programista będzie mógł określić, jak zaimplementować ten przypadek.
NDEBUG
Klasa assert makro wiąże się z niewielkim kosztem wydajności, który jest ponoszony za każdym razem, gdy sprawdzany jest warunek potwierdzenia. Co więcej, asercje nie powinny (w idealnym przypadku) nigdy nie występować w kodzie produkcyjnym (ponieważ Twój kod powinien już zostać dokładnie przetestowany). W związku z tym większość programistów woli, aby twierdzenia były aktywne tylko w kompilacjach debugowania. C++ ma wbudowany sposób wyłączania asercji w kodzie produkcyjnym: jeśli makro preprocesora NDEBUG jest zdefiniowane, makro asercji zostaje wyłączone.
Większość IDE jest ustawiona NDEBUG domyślnie jako część ustawień projektu dla konfiguracji wersji. Na przykład w programie Visual Studio na poziomie projektu ustawiane są następujące definicje preprocesora: WIN32;NDEBUG;_CONSOLE. Jeśli używasz programu Visual Studio i chcesz, aby Twoje potwierdzenia były wyzwalane w kompilacjach wersji, musisz usunąć NDEBUG to ustawienie.
Jeśli używasz IDE lub systemu kompilacji, który nie definiuje automatycznie NDEBUG w konfiguracji wydania, musisz ręcznie dodać je w ustawieniach projektu lub kompilacji.
Wskazówka
W celach testowych możesz włączyć lub wyłączyć asercje w danej jednostce tłumaczenia. Aby to zrobić, umieść jeden z następujących elementów w osobnej linii przed jakimkolwiek #includes: #define NDEBUG (aby wyłączyć potwierdzenia) lub #undef NDEBUG (aby włączyć potwierdzenia). Upewnij się, że nie kończysz linii średnikiem.
np.
#define NDEBUG // disable asserts (must be placed before any #includes)
#include <cassert>
#include <iostream>
int main()
{
assert(false); // won't trigger since asserts have been disabled in this translation unit
std::cout << "Hello, world!\n";
return 0;
}C++ ma również inny typ asercji o nazwie static_assert. A static_assert to asercja sprawdzana w czasie kompilacji, a nie w czasie wykonywania, przy czym niepowodzenie static_assert powoduje błąd kompilacji. W przeciwieństwie do potwierdzenia, które jest zadeklarowane w nagłówku <cassert>, static_assert jest słowem kluczowym, więc aby go użyć, nie trzeba dołączać nagłówka.
A static_assert przybiera następującą formę:
static_assert(condition, diagnostic_message)
Jeśli warunek nie jest spełniony, drukowany jest komunikat diagnostyczny. Oto przykład użycia static_assert w celu zapewnienia określonego rozmiaru typów:
static_assert(sizeof(long) == 8, "long must be 8 bytes");
static_assert(sizeof(int) >= 4, "int must be at least 4 bytes");
int main()
{
return 0;
}Na komputerze autora podczas kompilacji pojawiają się błędy kompilatora:
1>c:\consoleapplication1\main.cpp(19): error C2338: long must be 8 bytes
Kilka przydatnych uwag na temat static_assert:
- Ponieważ
static_assertjest oceniany przez kompilator, warunek musi być wyrażeniem stałym. static_assertmożna umieścić w dowolnym miejscu pliku kodu (nawet w globalnym przestrzeni nazw).static_assertnie jest dezaktywowany w kompilacjach wydań (jak normalneassertjest).- Ponieważ kompilator przeprowadza ocenę, nie ma kosztów czasu wykonania dla
static_assert.
Wcześniej niż C++17, komunikat diagnostyczny musi zostać dostarczony jako drugi parametr. Od C++17 dostarczanie komunikatu diagnostycznego jest opcjonalne.
Najlepsza praktyka
Zamawiaj static_assert za assert() jeśli to możliwe.
Twierdzenia a obsługa błędów
Twierdzenia i obsługa błędów są na tyle podobne, że ich cele można pomylić, więc wyjaśnijmy to.
Twierdzenia służą do wykrywać błędy programistyczne podczas programowania poprzez dokumentowanie założeń dotyczących rzeczy, które nigdy nie powinny się wydarzyć. A jeśli tak się stanie, będzie to wina programisty. Asercje nie pozwalają na naprawę po błędach (w końcu, jeśli coś nie powinno się nigdy wydarzyć, nie ma potrzeby się po tym naprawiać). Ponieważ asercje są zazwyczaj kompilowane w kompilacjach wydań, można umieścić ich wiele, nie martwiąc się o wydajność, więc nie ma powodu, aby nie używać ich swobodnie.
Obsługa błędów jest używana, gdy musimy sprawnie obsłużyć przypadki, które mogą się zdarzyć (choć rzadko) w kompilacji wydania. Mogą to być problemy, które można naprawić (gdzie program może nadal działać) lub problemy, których nie da się naprawić (gdzie program musi zostać zamknięty, ale możemy przynajmniej wyświetlić ładny komunikat o błędzie i upewnić się, że wszystko zostało poprawnie wyczyszczone). Wykrywanie i obsługa błędów wiąże się zarówno z kosztem wydajności w czasie wykonywania, jak i kosztem czasu programowania.
W niektórych przypadkach nie jest jasne, co powinniśmy zrobić. Rozważmy taką funkcję:
double getInverse(double x)
{
return 1.0 / x;
}Jeśli x Jest 0.0, ta funkcja będzie działać nieprawidłowo i musimy się przed tym zabezpieczyć. Czy powinniśmy używać obsługi asercji czy błędów? Najlepszą odpowiedzią może być „oba”.
Jeśli podczas debugowania funkcja ta zostanie wywołana gdy x Jest 0.0, oznacza to, że gdzieś w naszym kodzie jest błąd i chcemy to natychmiast wiedzieć. Zatem stwierdzenie jest zdecydowanie właściwe.
Jednak może się to również rozsądnie zdarzyć w przypadku kompilacji wydania (np. wzdłuż niejasnej ścieżki, której nie testowaliśmy). Jeśli asert zostanie skompilowany i nie mamy obsługi błędów, wówczas ta funkcja zwróci coś nieoczekiwanego i będzie działać nieprawidłowo. W takim przypadku prawdopodobnie lepiej jest to wykryć i zająć się tą sprawą.
Nasza wynikowa funkcja może wyglądać następująco:
double getInverse(double x)
{
assert(x != 0.0);
if (x == 0.0)
// handle error somehow (e.g. throw an exception)
return 1.0 / x;
}Wskazówka
Biorąc pod uwagę to, sugerujemy, co następuje:
- Używaj asercji do wykrywania błędów programistycznych, nieprawidłowych założeń lub warunków, które nigdy nie powinny wystąpić w poprawnym kodzie. Naprawienie tych błędów jest obowiązkiem programisty, dlatego chcemy je wcześnie wykryć.
- Użyj obsługi błędów w przypadku problemów, które według nas mogą wystąpić podczas normalnego działania programu.
- Użyj obu w przypadkach, gdy coś nie powinno się wydarzyć, ale chcemy, aby poniosło porażkę, jeśli tak się stanie.
Niektóre twierdzą ograniczenia i ostrzeżenia
Istnieje kilka pułapek i ograniczeń, których należy przestrzegać twierdzi. Po pierwsze, samo twierdzenie może być nieprawidłowo napisane. Jeśli tak się stanie, asercja albo zgłosi błąd tam, gdzie go nie ma, albo nie zgłosi błędu tam, gdzie taki istnieje.
Po drugie, twoje assert() wyrażenia nie powinny powodować żadnych skutków ubocznych, ponieważ wyrażenie asert nie zostanie ocenione, gdy NDEBUG jest zdefiniowane (a zatem efekt uboczny nie zostanie zastosowany). W przeciwnym razie to, co testujesz w konfiguracji debugowania, nie będzie takie samo jak w konfiguracji wydania (zakładając, że dostarczasz z NDEBUG).
Pamiętaj także, że funkcja abort() zakończa działanie programu natychmiast, bez możliwości dalszego czyszczenia (np. zamknięcia pliku lub bazy danych). Z tego powodu asercji należy używać tylko w przypadkach, gdy jest mało prawdopodobne, aby uszkodzenie nastąpiło w przypadku nieoczekiwanego zakończenia programu.

