
В коде часто встречаются проверки вида:
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)
Комментарии (20)

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А что лучше, остановить двигатель на ходу, или, заведомо зная, что подача топлива работает неверно, продолжать упорно лить до гидроудара?

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

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

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

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

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

S1onnach
03.11.2025 10:42Послушай птичка, что я тебе скажу. Щас за пять минут придрочимся к ассертам и полетим..
OlegMax
Тема действительно важная, но я бы посмотрел на другие аспекты:
Принципиально, чтобы программисту было максимально просто добавлять проверку инвариантов, не задумываясь, куда что логируется и выводится.
Очень часто приходим к тому, что простой остановки программы недостаточно, а нужно куда-то сохранить информацию об ассерте, то есть стандартный
assertне подходит, нужен свой макрос.Отключение ассертов в релизе тоже не очевидная необходимость. Например, в Chromium недавно стали уходить от debug-only проверок. Поверьте, производительность им тоже важна, но, видимо, баланс пользы сложился в сторону проверок.
viordash Автор
можете указать, где почитать про это, интересны мотивы?
Быстрый поиск не нашел по этой теме ничего толкового.
OlegMax
Можно начать отсюда -https://groups.google.com/a/chromium.org/g/cxx/c/cy579lMzgTw/m/9YzIgUMkAAAJ