
В коде часто встречаются проверки вида:
void process(Config* config) {
if (config == nullptr) { // Но config создается в этом же модуле!
log_error("Config is null");
return;
}
// ...
}
хотя можно написать более явно и эффективно:
void process(Config* config) {
assert(config != nullptr && "Config cannot be null");
// ...
}
Или другой пример:
void processArray(int* array, size_t size) {
if (array == nullptr && size > 0) {
log_error("The pointer must not be nullptr if size > 0");
return;
}
// ...
}
гораздо понятней будет:
void processArray(int* array, size_t size) {
assert(array != nullptr || size == 0);
// ...
}
Такие if лишь загромождают код и создают иллюзию безопасности. Гораздо эффективнее использовать assert - он документирует ваши намерения, а его код не попадает в релизную сборку.
Плюсы такого решения:
Нет мусора в релизе. Каждый
if, проверяющий условия, которые не могут нарушиться в корректной программе - это dead code в релизе.Производительность. В "горячих" циклах, которые выполняются миллионы раз, замена
ifна assert уберет в релизе (NDEBUG) миллионы этих лишних проверок.Меньше размер исполняемого файла.
Код как документация.
assert(config != nullptr)чётко заявляет: "указатель не может быть нулевым!". Обычныйifв такой ситуации лишь намекает: "возможно, здесь нужно обработать ошибку".Лучше падение, чем тихая ошибка. Если ваше утверждение ложно - это баг. Лучше упасть сразу в Debug, чем в релизе тихо логировать ошибку и продолжать работу с некорректным состоянием.
Но есть и минусы:
Не использовать функции внутри assert. Код внутри assert должен быть идемпотентным (не иметь побочных эффектов). Например:
assert(initialize_connection() == SUCCESS);илиassert(my_vector.pop_back() == value);В релизной сборке эти функции не будут вызваны, что приведет к непредсказуемому поведению. Все проверки с побочными эффектами должны быть обычнымиif. Или вassertпередавать только результаты функций.Необходимость хорошего покрытия unit-тестами.
Тестировать в обеих конфигурациях (Debug и Release). В Debug вы проверяете, что
assertсрабатывают там, где нужно. В Release - что программа стабильно работает и без них.Небольшое отличие в поведении программы в Debug и Release.
Но нужно всегда помнить: assert НЕ для валидации пользовательского ввода! Его задача ловить ошибки программиста, а не ошибки пользователя или внешних систем. Если кратко: assert - для программиста, if - для пользователя.
Как работает магия с NDEBUG?
Секрет в стандартном макросе препроцессора, который так и называется - NDEBUG (сокращение от "No Debug"). Типичная реализация выглядит примерно так:
#ifdef NDEBUG
// Если NDEBUG определен, assert превращается в "ничто"
#define assert(condition) ((void)0)
#else
#define assert(condition) \
do { \
if (!(condition)) { \
std::abort(); \
} \
} while (0)
#endif
В Release-сборке этот макрос обычно добавляется к флагам компиляции (например, -DNDEBUG в GCC/Clang/MSVC).
Проблема неиспользуемых переменных.
Если в assert передается переменная, которая больше нигде не используется, при компиляции с NDEBUG можно получить предупреждение:
warning: unused variable 'config' [-Wunused-variable]
Избавиться от предупреждений можно обернув assert в свой макрос, например:
#define ASSERT(x) \
do { \
bool cond = (x); \
(void)cond; \
assert(cond); \
} while (0)
Использование (void)cond подавит это предупреждение. Также поможет добавление атрибутов к таким переменным: __attribute((unused) или [[maybe_unused]].
Тестирование срабатывания assert.
Проверка того, что в определенных условиях действительно срабатывает assert, задача нетривиальная, но решаемая. Вот несколько практических подходов:
Использовать
GoogleTestиASSERT_DEATH/EXPECT_DEATHИспользование внешних стандартных библиотек (
-nostdlib). И затем mock-ировать функцию abort();Попытаться переопределить макрос assert, например:
#define assert(condition) \
do { \
if (!(condition)) { \
mock_abort(); \
} \
} while (0)
Комментарии (41)

Goron_Dekar
03.11.2025 10:42Ни разу не сталкивался с проектом, где мог бы работать стандартный assert. Очень часто ошибку надо не только поймать, но и о ней всем и правильно сообщить.
Лучшее применение ассертам - контракт. Но в плюсах пока нормальных контрактов не завезли, и если хотите их реализовывать - проще передавать уже валидированные (желательно в компилтайме) объекты, чем валидировать по месту приёма.

vadimr
03.11.2025 10:42Далеко не везде приемлема ситуация, когда программе позволительно падать при ошибках программиста. Особенно странно это выглядит с тегом "системное программирование". Вы бы хотели, чтобы у вас, например, операционная система падала на ассертах?

bogolt
03.11.2025 10:42И кстати не раз видел вываливающиеся ассерты в разном софте. Если честно подход не обрабатывать ошибку в релизе очень странный, и по-моему плохо применим в реальной жизни. Ну или точнее может быть применим где-то в шаблонном программировании, где у нас ассерт тестирует размеры переменных например - нечто что можно сделать в процессе компиляции, но не процессе работы.

vadimr
03.11.2025 10:42И кстати не раз видел вываливающиеся ассерты в разном софте.
В джавовском софте обычно это можно видеть :) Но при этом, как правило, программы продолжают работу, в отличие от описанной в статье логики.

viordash Автор
03.11.2025 10:42при ошибках программиста
согласен с вами, то что не везде падение позволительно. Но для анализа того, насколько неверный код меняет логику программы, нужно слишком много ресурсов. Проще упасть и затем исправить ошибку.

vadimr
03.11.2025 10:42Если программа находится перед программистом, то проще упасть и исправить. А если управляет каким-то HA процессом (для примера, подачей топлива в двигатель автомобиля), то совсем неправильно падать, а надо предусматривать код для парирования ошибок программиста.
Везде, конечно, своя специфика, но поддержу мнение предыдущего оратора @Goron_Dekar – я тоже не сталкивался с проектами, где был бы оправдан стандартный assert с падением (кроме совсем небольших программ).

viordash Автор
03.11.2025 10:42но есть ли гарантия что программа верно подает топливо? Раз есть логическая ошибка в коде.

vadimr
03.11.2025 10:42Даже если программа не совсем верно подаёт топливо, то это лучше, чем вообще заглушить двигатель на ходу. В таких случаях предусматривается несколько контуров обработки, резервирующих друг друга, и у вас, допустим, вырастет расход бензина, но автомобиль продолжит движение.

apevzner
03.11.2025 10:42А что лучше, остановить двигатель на ходу, или, заведомо зная, что подача топлива работает неверно, продолжать упорно лить до гидроудара?

equmag
03.11.2025 10:42Продолжать лить, вывести предупреждение, что нужно срочно съехать на обочину и остановить машину, разумеется. Смысл таких обработок, что бы смягчить последствия и дать минимальную работоспособность для ручной обработки пользователем. Мгновенный abort практически всегда вызывает куда более катастрофические последствия, чем некорректная работа, в данном случае авария с потенциально летальным исходом
abort всегда можно вызвать и без assert, если нужно, но с ним как раз получится, что указанная ошибка в релизе обработана не будет вовсе

apevzner
03.11.2025 10:42Как вариант, критически важная программа на assert-е может упасть, а внешняя по отношению к ней система запустить вместо неё аварийную замену (или просто перезапустить её. Или перезапустить несколько раз, а если не помогло - перезапустить аварийную замену).
Что безопаснее, при обнаружении внутренних ошибок - проглотить их и сделать вид, что всё ОК, когда на самом деле не ОК, или явно раскрутить цепочку обработки отказа?

vadimr
03.11.2025 10:42Именно потому, что ошибки не надо глотать, а надо разбираться с ними, внешний обработчик не очень подходит, так как информация о внутреннем состоянии программы теряется.

apevzner
03.11.2025 10:42Это эквивалент kernel panic на Linux или BSOD на венде.
А вы бы хотели, чтобы вместо этого система продолжила работать, но делала бы что-нибудь нввидимо и неправильно? Например, какая-нибудь важная ядерная нитка подвисла бы навечно и перестала бы отрабатывать относящиеся к ней запросы. И в результате, например, система делала бы вид, что работает, но записи на диск накапливались бы в памяти, а на диск бы не попадали.
Может всё же в безнадёжной ситуации лучше тогось, чем чтобы программа прикидывалась живой?

vadimr
03.11.2025 10:42Смотря какая программа и в какой ситуации. Если вы откроете журнал ядра ОС, то увидите, что ошибки там происходят постоянно. А kernel panic - совсем уж редкий вариант.

apevzner
03.11.2025 10:42Ошибки ошибкам рознь.
Если это какие-то неправильные данные, пришедшие снаружи, некорректное или неожиданное поведение аппаратуры, нехватка каких-то ресурсов, типа памяти и т.п., то для ядра это - нормальные, штатные ситуации, которые ядро должно корректно отрабатывать.
Но если ошибка именно во внутренней логике кода, как вы прикажете её отрабатывать?

Goron_Dekar
03.11.2025 10:42BSOD выводит информацию, генерирует qr, паркует харды, сбрасывает кэши. Это уже не assert.

geher
03.11.2025 10:42Может всё же в безнадёжной ситуации лучше тогось, чем чтобы программа прикидывалась живой?
Случаи бывают разные. Если имеем управление автономным аппаратом, то в любой ситуации ассерт - однозначно кранты, а без него может и вытянет. Даже если полагаться на перезапуск упавшего, то, что отвнчает за перезапуск, падать не должно.
Другой случай, когда ошибка - это не совсем всё, а только потеря части результата. Игнорирование ошибки может позволить таки получить оставшуюся часть.

aamonster
03.11.2025 10:42Если программист пишет if, а не assert – скорей всего, у него есть на это причины. Например, сегодня переменная точно инициализируется в том же модуле, а завтра может и нет – в зависимости от какого-то неочевидного условия, и при проверке на дебаг-версии на это не наткнулись, а в релизе выстрелит.
Assert – пометки "для себя", на совсем уж очевидные случаи. If – уже более серьёзная проверка. Ну и третий случай – if, который уберёт компилятор (ибо увидит, что 5 строчками выше переменной присваивается значение).

S1onnach
03.11.2025 10:42Послушай птичка, что я тебе скажу. Щас за пять минут придрочимся к ассертам и полетим..

Tuxman
03.11.2025 10:42process(Config* config)
Если передавать как ссылку, то и проверять на Nullptr не надо ;-)

Kyoki
03.11.2025 10:42гораздо понятней будет
А потом падает, т.к. при сборке на прод забыли файл положить. Или создание конфига не удалось... и пофиг, что в том же модуле. if и assert невзаимозаменяемые в общем случае.

Daemonis
03.11.2025 10:42А какой смысл ей не падать, если нет необходимого файла?

Goron_Dekar
03.11.2025 10:42Поискать файл в другом месте? Сообщить, что нет файла? Создать файл по-умолчанию?

sci_nov
03.11.2025 10:42Ассерты по большей части в тестах находятся. А в боевом коде их почти нет - там есть макросы, которые логируют ошибки, и в зависимости от макроса либо приводят к падению (exception имеется ввиду), либо нет. Exceptions либо обрабатываются выше, либо нет.
В боевом коде ассерты могут быть на время разработки какой либо фичи. Потом их убирают и заменяют тестами.

AndronNSK
03.11.2025 10:42Очень странная статья. За предлагаемый подход я разворачиваю MRы.
Хочется ассерт - ставится ассерт, а после него лог и выход из функции.

makaedgar
03.11.2025 10:42К сожалению, аssert не спасет от разыменования nullptr в релизе. А в C++26 завезли контракты специально для этой цели.

viordash Автор
03.11.2025 10:42Да, assert не спасает в релизе, он и не должен этого делать. Его задача отловить баг в рантайме на этапе разработки. Наврятли где-то вся разработка сразу в релизе ведется.

makaedgar
03.11.2025 10:42В реальном проекте покрыть всю логику и все бранчи тестами нереально. Правильная деградация - это тоже часть логики программы, тогда как ассерт - это верный путь к UB на проде, и может быть ещё хуже. Для горячих мест окей, но много ли их реально?
А так, конечно, разработка ведётся также и на проде (в релизе). Пользователи присылают баг-репорты, а анализ логов и корок даёт много полезной инфы, причём в реальных юзкейсах. Чем не тестирование!
Мы не тестируем на животных


equmag
03.11.2025 10:42assert имеет на самом деле весьма узкое применение, аргументация о падении программы в дебаге понятна, но в дебаге и не суть важно, если она продолжит работать некорректно, но с логами. А вот отсутствие проверок в рантайме может привести к катастрофическим последствиям. И я могу сходу представить огромную палитру багов от разработчика, которые очень сильно пострадают от assert. assert актуален только для стабильных compile-time ошибок, что большая редкость.
Допустим у нас какая-то data-driven разработка, мы читаем информацию из xml, собранного нами же самими. Делается assert на ошибку разработчика, мы множество раз протестировали в дебаге все работает. А потом в рантайме файл меняется, assert в релизе не срабатывает и у нас посыпалась вся логика без какой либо обработки. И все что бы сохранить парочку cpu инструкций!
Не чушь ли, товарищи. assert имеет место быть, но в узком направлении ошибок, статья гипер-обобщает, на мой взгляд
OlegMax
Тема действительно важная, но я бы посмотрел на другие аспекты:
Принципиально, чтобы программисту было максимально просто добавлять проверку инвариантов, не задумываясь, куда что логируется и выводится.
Очень часто приходим к тому, что простой остановки программы недостаточно, а нужно куда-то сохранить информацию об ассерте, то есть стандартный
assertне подходит, нужен свой макрос.Отключение ассертов в релизе тоже не очевидная необходимость. Например, в Chromium недавно стали уходить от debug-only проверок. Поверьте, производительность им тоже важна, но, видимо, баланс пользы сложился в сторону проверок.
viordash Автор
можете указать, где почитать про это, интересны мотивы?
Быстрый поиск не нашел по этой теме ничего толкового.
OlegMax
Можно начать отсюда -https://groups.google.com/a/chromium.org/g/cxx/c/cy579lMzgTw/m/9YzIgUMkAAAJ
Tuxman
Assert вызовет abort(), тот посылает SIGABRT, и по-умолчанию создаётся core dump.
Обычно вы запускаете программу/демон из какой-то обёртки на shell/python, и если программа завершила аварийно, то можно посмотреть создалась ли кора, и зайти gdb bt, и прислать, например, на email.