Domyślnym zachowaniem assert
w C ++ jest brak działania w kompilacjach wersji. Zakładam, że dzieje się tak ze względu na wydajność i być może w celu uniemożliwienia użytkownikom wyświetlania nieprzyjemnych komunikatów o błędach.
Twierdzę jednak, że sytuacje, w których assert
strzeliłby, ale został wyłączony, są jeszcze bardziej kłopotliwe, ponieważ aplikacja najprawdopodobniej ulegnie awarii w jeszcze gorszym stopniu, ponieważ część niezmiennika została zerwana.
Dodatkowo argument wydajności dla mnie liczy się tylko wtedy, gdy jest to wymierny problem. Większość assert
s mojego kodu nie jest dużo bardziej złożona niż
assert(ptr != nullptr);
co będzie miało niewielki wpływ na większość kodu.
To prowadzi mnie do pytania: czy asercje (czyli koncepcja, a nie konkretna implementacja) powinny być aktywne w kompilacjach wersji? Dlaczego nie)?
Należy pamiętać, że to pytanie nie dotyczy sposobu włączania asercji w kompilacjach wersji (takich jak #undef _NDEBUG
implementacja asercji lub korzystanie z niej). Co więcej, nie chodzi o włączanie asercji w kodzie biblioteki trzeciej / standardowym, ale w kodzie kontrolowanym przeze mnie.
źródło
Odpowiedzi:
Klasyk
assert
jest narzędziem ze starej standardowej biblioteki C, a nie z C ++. Jest nadal dostępny w C ++, przynajmniej z powodów kompatybilności wstecznej.Nie mam pod ręką dokładnej osi czasu standardowych bibliotek C, ale jestem prawie pewien, że
assert
był dostępny wkrótce po wejściu K&R C na rynek (około 1978 r.). W klasycznym języku C do pisania solidnych programów dodawanie testów wskaźnika NULL i sprawdzanie granic tablic należy przeprowadzać znacznie częściej niż w C ++. Kosztów testów wskaźnika NULL można uniknąć, stosując odniesienia i / lub inteligentne wskaźniki zamiast wskaźników, a przy użyciustd::vector
sprawdzania granic tablicy często nie jest konieczne. Co więcej, wydajność osiągnięta w 1980 roku była zdecydowanie ważniejsza niż dzisiaj. Myślę więc, że jest bardzo prawdopodobne, że „assert” został domyślnie zaprojektowany tak, aby był aktywny tylko w kompilacjach debugowania.Co więcej, do obsługi prawdziwych błędów w kodzie produkcyjnym funkcja, która po prostu testuje jakiś warunek lub niezmiennik i powoduje awarię programu, jeśli warunek nie jest spełniony, w większości przypadków nie jest wystarczająco elastyczna. W przypadku debugowania jest to prawdopodobnie w porządku, ponieważ ten, kto uruchamia program i zauważa błąd, zwykle ma pod ręką debugger do analizy tego, co się dzieje. Jednak w przypadku kodu produkcyjnego rozsądnym rozwiązaniem musi być funkcja lub mechanizm, który
testuje jakiś warunek (i zatrzymuje wykonywanie w zakresie, w którym warunek się nie powiedzie)
wyświetla wyraźny komunikat o błędzie w przypadku, gdy warunek się nie utrzymuje
pozwala zewnętrznemu zasięgowi na przesłanie komunikatu o błędzie i wyprowadza go do określonego kanału komunikacyjnego. Ten kanał może być czymś w rodzaju stderr, standardowego pliku dziennika, okna komunikatu w programie GUI, ogólnego oddzwaniania do obsługi błędów, kanału błędów w sieci lub czegokolwiek, co najlepiej pasuje do konkretnego oprogramowania.
pozwala zewnętrznemu zakresowi na podstawie poszczególnych przypadków zdecydować, czy program powinien zakończyć się z wdziękiem, czy kontynuować.
(Oczywiście są też sytuacje, w których natychmiastowe zakończenie programu w przypadku niespełnienia warunku jest jedyną sensowną opcją, ale w takich przypadkach powinno to nastąpić również w kompilacji wersji, a nie tylko w wersji debugowania).
Od klasyki
assert
wersja nie udostępnia tych funkcji, nie jest dobrze dopasowana do kompilacji wersji, zakładając, że kompilacja wersji jest tym, co wdraża się w produkcji.Teraz możesz zapytać, dlaczego w standardowej bibliotece języka C nie ma takiej funkcji ani mechanizmu, który zapewnia taką elastyczność. W rzeczywistości w C ++ istnieje standardowy mechanizm, który ma wszystkie te funkcje (i więcej) i wiesz o tym: nazywa się to wyjątkami .
Jednak w języku C trudno jest wdrożyć dobry, uniwersalny mechanizm ogólnego przeznaczenia do obsługi błędów ze wszystkimi wymienionymi funkcjami z powodu braku wyjątków jako części języka programowania. Dlatego większość programów w C ma własne mechanizmy obsługi błędów z kodami powrotu, „goto”, „długimi skokami” lub ich kombinacją. Są to często pragmatyczne rozwiązania, które pasują do określonego rodzaju programu, ale nie są „wystarczająco ogólne, aby zmieściły się w bibliotece standardowej C.
źródło
#include <cassert>
). To całkowicie ma teraz sens.assert
jest również niewłaściwym narzędziem do tego (zgłoszenie wyjątku może być rzeczywiście niewłaściwą reakcją ). Jestem jednak całkiem pewien, że w rzeczywistościassert
był również używany do wielu testów w C, gdzie w C ++ można by dziś używać wyjątków.Jeśli okaże się, że chcesz, aby aserty były włączone w wydaniu, które poprosiłeś, aby wykonały złą pracę.
Chodzi o to, że nie są włączone w wydaniu. Pozwala to na testowanie niezmienników podczas programowania przy użyciu kodu, który w innym przypadku musiałby być kodem rusztowania. Kod, który należy usunąć przed wydaniem.
Jeśli masz coś, co Twoim zdaniem powinno zostać przetestowane nawet podczas wydania, napisz kod, który to przetestuje. The
If throw
Konstrukt działa bardzo dobrze. Jeśli chcesz powiedzieć coś innego niż inne rzuty, po prostu użyj opisowego wyjątku, który mówi, co chcesz powiedzieć.Nie chodzi o to, że nie możesz zmienić sposobu, w jaki używasz zapewnień. Chodzi o to, że nie przynosi ci to nic użytecznego, jest sprzeczne z oczekiwaniami i nie pozostawia w żaden sposób czystego sposobu robienia tego, co powinny robić twierdzenia. Dodaj testy, które są nieaktywne w wydaniu.
Dlaczego nie ma parametru relase_assert? Szczerze mówiąc, ponieważ twierdzenia nie są wystarczające do produkcji. Tak, istnieje potrzeba, ale nic nie spełnia tej potrzeby. Och, na pewno możesz zaprojektować własny. Mechanicznie twoja funkcja throwIf wymaga po prostu bool i wyjątku do rzucenia. I to może pasować do twoich potrzeb. Ale tak naprawdę ograniczasz projekt. Dlatego nie dziwi mnie, że w twojej bibliotece języków nie ma systemu zgłaszania wyjątków. Z pewnością nie jest tak, że nie możesz tego zrobić. Inni mają . Ale zajmowanie się przypadkiem, w którym coś idzie nie tak, to 80% pracy dla większości programów. I jak dotąd nikt nie pokazał nam dobrego, uniwersalnego rozwiązania dla wszystkich rozwiązań. Skuteczne radzenie sobie z tymi przypadkami może się skomplikować. Gdybyśmy mieli puszkowany system release_assert, który nie spełniałby naszych potrzeb, myślę, że przyniósłby więcej szkody niż pożytku. Prosisz o dobrą abstrakcję, która oznaczałaby, że nie musiałbyś myśleć o tym problemie. Ja też chcę, ale nie wygląda na to, żebyśmy tam byli.
Dlaczego twierdzenia są wyłączone w wydaniu? Aserty powstały u szczytu wieku kodu rusztowania. Kod, który musieliśmy usunąć, ponieważ wiedzieliśmy, że nie chcemy go w produkcji, ale wiedzieliśmy, że chcemy uruchomić programowanie, aby pomóc nam znaleźć błędy. Aserty były czystszą alternatywą dla
if (DEBUG)
wzorca, który pozwala nam pozostawić kod, ale go wyłączyć. Było to przed rozpoczęciem testów jednostkowych jako głównego sposobu na oddzielenie kodu testowego od kodu produkcyjnego. Aserty są nadal używane, nawet przez ekspertów testujących jednostki, zarówno w celu wyjaśnienia oczekiwań, jak i uwzględnienia przypadków, w których są one lepsze niż testy jednostkowe.Dlaczego nie zostawić kodu debugowania w produkcji? Ponieważ kod produkcyjny nie powinien zawstydzać firmy, nie formatować dysku twardego, nie uszkadzać bazy danych i nie wysyłać wiadomości e-mail do prezesa. Krótko mówiąc, miło jest móc pisać kod debugujący w bezpiecznym miejscu, w którym nie musisz się o to martwić.
źródło
catch(...)
zrobić.if (DEBUG)
do kontrolowania czegoś innego niż kod debugowania. Mikrooptymalizacja może w twoim przypadku nic nie znaczyć. Ale ten idiom nie powinien być obalony tylko dlatego, że go nie potrzebujesz.assert
ale o koncepcji twierdzenia. Nie chcę nadużywaćassert
ani dezorientować czytelników. Chciałem przede wszystkim zapytać, dlaczego tak jest. Dlaczego nie ma żadnych dodatkowychrelease_assert
? Czy nie ma takiej potrzeby? Jakie jest uzasadnienie twierdzenia o wyłączeniu w wydaniu?You're asking for a good abstraction.
Nie jestem tego pewny. Chcę przede wszystkim poradzić sobie z problemami, których nie można odzyskać i które nigdy nie powinny się zdarzyć. Weź to razem z,Because production code needs to [...] not format the hard drive [...]
i powiedziałbym, że w przypadkach, w których niezmienniki są zepsute, wezmę w dowolnym momencie uwagę na UB.Asercje to narzędzie do debugowania , a nie technika programowania defensywnego. Jeśli chcesz przeprowadzić sprawdzanie poprawności we wszystkich okolicznościach, wykonaj sprawdzanie poprawności, pisząc polecenie warunkowe - lub utwórz własne makro, aby zmniejszyć płytkę kotłową.
źródło
assert
jest formą dokumentacji, podobnie jak komentarze. Podobnie jak komentarze, zwykle nie wysyłałbyś ich do klientów - nie należą one do kodu wersji.Ale problem z komentarzami polega na tym, że mogą stać się nieaktualne, a jednak pozostaną. Dlatego twierdzenia są dobre - sprawdzane są w trybie debugowania. Gdy assert stanie się przestarzały, szybko go odkryjesz i nadal będziesz wiedział, jak go naprawić. Ten komentarz, który stał się nieaktualny 3 lata temu? Zgadnijcie.
źródło
Jeśli nie chcesz, aby „twierdzenie” było wyłączone, możesz napisać prostą funkcję, która ma podobny efekt:
Oznacza to, że
assert
jest na testach, gdzie zrobić chcą im odejść w dostarczonym produkcie. Jeśli chcesz, aby ten test był częścią określonego zachowania programu,assert
jest to niewłaściwe narzędzie.źródło
Nie ma sensu argumentować, co powinien zrobić aser, robi to, co robi. Jeśli chcesz czegoś innego, napisz własną funkcję. Na przykład mam Assert, który zatrzymuje się w debuggerze lub nic nie robi, mam AssertFatal, który spowoduje awarię aplikacji, mam funkcje boolowe Asserted i AssertionFailed, które potwierdzają i zwracają wynik, dzięki czemu mogę zarówno potwierdzić, jak i poradzić sobie z sytuacją.
W przypadku każdego nieoczekiwanego problemu musisz zdecydować, jak najlepiej poradzić sobie zarówno programista, jak i użytkownik.
źródło
verify(cond)
byłby to normalny sposób na potwierdzenie i zwrócenie wyniku?Jak zauważyli inni,
assert
jest to twój ostatni bastion obrony przed błędami programisty, które nigdy nie powinny się zdarzyć. Są to kontrole poczytalności, które, miejmy nadzieję, nie powinny zawieść w lewo i w prawo do czasu wysyłki.Został również zaprojektowany tak, aby można go było pominąć w wersjach stabilnych, bez względu na powody, dla których programiści mogą się okazać przydatni: estetykę, wydajność, cokolwiek chcą. Jest to część tego, co odróżnia kompilację debugowania od kompilacji wersji, a z definicji kompilacja wersji jest pozbawiona takich twierdzeń. Jest więc odwrócenie tego projektu, jeśli chcesz wydać analogiczną „kompilację wydania z zapewnieniami w miejscu”, która byłaby próbą kompilacji wersji z
_DEBUG
definicją preprocesora i nieNDEBUG
zdefiniowaną; tak naprawdę nie jest to już kompilacja wydania.Projekt rozciąga się nawet na standardową bibliotekę. W bardzo prosty przykład wśród wielu, wielu implementacjach
std::vector::operator[]
będzieassert
sprawdzenia dokonywane aby upewnić się, że nie sprawdzasz wektor poza granicami. Biblioteka standardowa zacznie działać znacznie, znacznie gorzej, jeśli włączysz takie kontrole w kompilacji wydania. Punkt odniesieniavector
przy użyciuoperator[]
a wypełnienie ctora takimi twierdzeniami zawartymi w stosunku do zwykłej starej tablicy dynamicznej często pokazuje, że tablica dynamiczna jest znacznie szybsza, dopóki nie wyłączysz takich kontroli, więc często wpływają one na wydajność w daleki, daleki od trywialnych sposobów. Kontrola zerowego wskaźnika tutaj i kontrola poza zakresem może faktycznie stać się ogromnym wydatkiem, jeśli takie kontrole są stosowane miliony razy w każdej ramce w krytycznych pętlach poprzedzających kod tak prosty, jak usunięcie dereferencji z inteligentnego wskaźnika lub dostęp do tablicy.Więc najprawdopodobniej potrzebujesz innego narzędzia do tego zadania i takiego, które nie zostało zaprojektowane tak, aby było pominięte w kompilacjach wersji, jeśli chcesz kompilacje wersji, które wykonują takie testy poprawności w kluczowych obszarach. Najbardziej przydatne dla mnie osobiście jest logowanie. W takim przypadku, gdy użytkownik zgłasza błąd, staje się znacznie łatwiej, jeśli dołącza dziennik, a ostatnia linia dziennika daje mi dużą wskazówkę, gdzie wystąpił błąd i co może być. Następnie, po odtworzeniu ich kroków w kompilacji debugowania, mogę również otrzymać błąd asercji, a ta awaria asercji dodatkowo daje mi ogromne wskazówki, aby usprawnić mój czas. Ponieważ jednak rejestrowanie jest stosunkowo drogie, nie używam go do przeprowadzania kontroli poczytalności na bardzo niskim poziomie, takich jak upewnienie się, że tablica nie jest dostępna poza granicami w ogólnej strukturze danych.
Jednak w końcu, w pewnym stopniu w zgodzie z tobą, widziałem rozsądny przypadek, w którym możesz wręczać testerom coś przypominającego kompilację debugowania podczas testów alfa, na przykład z małą grupą testerów alfa, którzy, powiedzmy, podpisali umowę NDA . Tam może usprawnić testy alfa, jeśli podasz testerom coś innego niż pełną wersję wydania z dołączonymi informacjami o debugowaniu wraz z niektórymi funkcjami debugowania / programowania, takimi jak testy, które można uruchomić i bardziej szczegółowe wyniki podczas uruchamiania oprogramowania. Przynajmniej widziałem, jak niektóre duże firmy produkujące gry robią takie rzeczy dla alfa. Ale dotyczy to testów alfa lub testów wewnętrznych, w których naprawdę próbujesz dać testerom coś innego niż kompilację wydania. Jeśli faktycznie próbujesz wysłać kompilację wersji, to z definicji nie powinna
_DEBUG
zdefiniowane lub to naprawdę mylące różnice między kompilacją „debugowania” i „wydania”.Jak wskazano powyżej, kontrole niekoniecznie są trywialne z punktu widzenia wydajności. Wiele z nich jest prawdopodobnie trywialnych, ale ponownie, nawet standardowa biblioteka lib używa ich i może to wpłynąć na wydajność w nieakceptowalny sposób dla wielu osób w wielu przypadkach, jeśli, powiedzmy, przechodzenie losowego dostępu
std::vector
zajęło 4 razy więcej czasu, co powinno być zoptymalizowaną wersją wydania ze względu na sprawdzanie granic, które nigdy nie powinno zawieść.W poprzednim zespole faktycznie musieliśmy sprawić, by nasza macierz i biblioteka wektorowa wykluczały niektóre twierdzenia na niektórych krytycznych ścieżkach, aby przyspieszyć kompilacje debugowania, ponieważ te spowalniały operacje matematyczne o ponad rząd wielkości do punktu, w którym było zaczynamy wymagać od nas czekania 15 minut, zanim będziemy mogli wczytać się w interesujący kod. Moi koledzy naprawdę chcieli po prostu usunąć
asserts
wprost, ponieważ odkryli, że samo to robi ogromną różnicę. Zamiast tego postanowiliśmy uniknąć krytycznych ścieżek debugowania. Kiedy sprawiliśmy, że te ścieżki krytyczne wykorzystywały dane wektorowe / macierzowe bezpośrednio, bez przechodzenia przez sprawdzanie granic, czas wymagany do wykonania pełnej operacji (która obejmowała więcej niż tylko matematykę wektor / macierz) skrócił się z minut do sekund. Jest to więc skrajny przypadek, ale zdecydowanie twierdzenia nie zawsze są nieistotne z punktu widzenia wydajności, a nawet bliskie.Ale to także sposób, w jaki
asserts
zostały zaprojektowane. Jeśli nie miały one tak dużego wpływu na wydajność na całym forum, mógłbym go faworyzować, gdyby zostały zaprojektowane jako coś więcej niż funkcja kompilacji debugowania lub moglibyśmy użyć tej funkcji,vector::at
która obejmuje sprawdzanie granic nawet w kompilacjach wersji i wyrzucanie poza granice dostęp, np. (ale z ogromnym hitem wydajności). Ale obecnie uważam, że ich projekt jest znacznie bardziej użyteczny, biorąc pod uwagę ich ogromny wpływ na wydajność w moich przypadkach, jako funkcję tylko do kompilacji debugowania, która jest pomijana, gdyNDEBUG
jest zdefiniowana. Przynajmniej w przypadku przypadków, z którymi pracowałem, w wydaniu kompilacji jest ogromna różnica, ponieważ wyklucza sprawdzanie poprawności, które nigdy nie powinno zawieść.vector::at
vs.vector::operator[]
Myślę, że rozróżnienie tych dwóch metod stanowi sedno tego, a także alternatywy: wyjątków.
vector::operator[]
implementacje zwykleassert
w celu upewnienia się, że dostęp poza granicami wyzwoli łatwo powtarzalny błąd podczas próby uzyskania dostępu do wektora poza granicami. Ale implementatorzy bibliotek robią to przy założeniu, że nie będzie to kosztować ani grosza w zoptymalizowanej wersji wydania.Tymczasem
vector::at
zapewnione jest to, że zawsze sprawdza się poza limitem i rzuca nawet w kompilacjach wersji, ale ma to negatywny wpływ na wydajność do tego stopnia, że często widzę o wiele więcej kodu używającegovector::operator[]
niżvector::at
. Wiele projektów C ++ przypomina ideę „płacić za to, czego używasz / potrzebujesz”, a wiele osób często faworyzujeoperator[]
, co nawet nie przeszkadza w sprawdzaniu granic w kompilacjach wersji, w oparciu o przekonanie, że nie nie trzeba sprawdzać granic w ich zoptymalizowanych kompilacjach wersji. Nagle, jeśli w kompilacjach wersji włączono asercje, wydajność tych dwóch będzie identyczna, a użycie wektora zawsze będzie wolniejsze niż tablicy dynamicznej. Tak więc ogromna część projektu i korzyści asercji opiera się na pomyśle, że stają się one darmowe w kompilacji wersji.release_assert
Jest to interesujące po odkryciu tych intencji. Oczywiście przypadki użycia dla każdego byłyby inne, ale myślę, że znajdę jakieś zastosowanie dla tego,
release_assert
który sprawdza i powoduje awarię oprogramowania, pokazując numer linii i komunikat o błędzie, nawet w kompilacjach wersji.Byłoby tak w przypadku niektórych niejasnych przypadków, w których nie chcę, aby oprogramowanie w pełni odzyskało wydajność, tak jak w przypadku zgłoszenia wyjątku. Chciałbym, aby w takich przypadkach zawiesił się, aby użytkownik mógł otrzymać numer linii, aby zgłosić, gdy oprogramowanie napotkało coś, co nigdy nie powinno się zdarzyć, nadal w sferze kontroli poprawności pod kątem błędów programisty, a nie zewnętrznych błędów wejściowych, takich jak wyjątki, ale na tyle tanie, że można to zrobić bez obawy o koszty wydania.
W rzeczywistości istnieją przypadki, w których znajdę poważną awarię z numerem linii i komunikatem o błędzie, które lepiej jest odzyskać od zgłoszonego wyjątku, który może być na tyle tani, aby utrzymać się w wydaniu. Są też przypadki, w których niemożliwe jest odzyskanie z wyjątku, na przykład błąd napotkany podczas próby odzyskania z istniejącego. Tam znalazłem idealne dopasowanie
release_assert(!"This should never, ever happen! The software failed to fail!");
i naturalnie byłoby tanie, ponieważ kontrola byłaby przeprowadzana przede wszystkim na wyjątkowej ścieżce i nie kosztowałaby nic w normalnych ścieżkach wykonania.źródło
Additionally, the performance argument for me only counts when it is a measurable problem.
Więc chodzi o to, aby zdecydować na podstawie każdego przypadku, kiedy wyciągnąć twierdzenie z wydania.assert
który opiera się na tych samych_DEBUG/NDEBUG
definicjach preprocesora, jak to, czego użyłbyś podczas używaniaassert
makra. Nie ma więc możliwości selektywnego włączania asercji tylko dla jednego fragmentu kodu bez włączenia go dla standardowej biblioteki lib, np. Zakładając, że używa standardowej biblioteki lib.assert
.vector::operator[]
ivector::at
, na przykład, miałby praktycznie taką samą wydajność, gdyby asercje były włączone w kompilacjach wersji, i nie byłoby sensuoperator[]
więcej używać z punktu widzenia wydajności, ponieważ i tak sprawdzałby granice.Piszę w Ruby, a nie w C / C ++, więc nie będę mówił o różnicy między twierdzeniami a wyjątkami, ale chciałbym mówić o tym jako o rzeczy, która zatrzymuje środowisko uruchomieniowe . Nie zgadzam się z większością powyższych odpowiedzi, ponieważ zatrzymanie programu z drukowaniem śladu wstecznego działa dla mnie dobrze jako technika programowania obronnego.
Jeśli istnieje sposób na wywołanie rutyny (absolutnie bez względu na to, jak jest ona składniowo i jeśli słowo „aser” jest używane lub istnieje w języku programowania lub dsl), oznacza to, że należy wykonać pewne czynności i produkt natychmiast wraca z „wydanej, gotowej do użycia” do „potrzebuje poprawki” - teraz albo przepisz ją do obsługi wyjątków, albo napraw błąd, który spowodował pojawienie się niewłaściwych danych.
Mam na myśli, że Assert nie jest czymś, z czym powinieneś często mieszkać i dzwonić - to sygnał stopu, który wskazuje, że powinieneś zrobić coś, aby to się nigdy więcej nie powtórzyło. A powiedzenie, że „wydanie kompilacji nie powinno mieć asercji” jest jak powiedzenie „wydanie kompilacji nie powinno zawierać błędów” - koleś, prawie niemożliwe jest uporanie się z tym.
Lub pomyśl o nich jako o „niepowodzeniu niezastosowanych i wykonanych przez użytkownika końcowego”. Nie możesz przewidzieć wszystkich rzeczy, które użytkownik zrobi z twoim programem, ale jeśli coś zbyt poważnego pójdzie nie tak, powinno się to zatrzymać - jest to podobne do sposobu budowania potoków kompilacji - zatrzymujesz proces i nie publikujesz, prawda? ? Asercja zmusza użytkownika do zatrzymania, zgłoszenia i oczekiwania na pomoc.
źródło