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 nie NDEBUG
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ędzie assert
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 odniesienia vector
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”.
Dlaczego ten kod należy usunąć przed wydaniem? Kontrole nie powodują znacznego obniżenia wydajności, a jeśli się nie powiedzie, zdecydowanie pojawia się problem, o którym wolałbym bardziej bezpośredni komunikat błędu.
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, gdy NDEBUG
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 zwykle assert
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ącego vector::operator[]
niż vector::at
. Wiele projektów C ++ przypomina ideę „płacić za to, czego używasz / potrzebujesz”, a wiele osób często faworyzuje operator[]
, 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.