В коде часто встречаются проверки вида:

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)


  1. OlegMax
    03.11.2025 10:42

    Тема действительно важная, но я бы посмотрел на другие аспекты:

    1. Принципиально, чтобы программисту было максимально просто добавлять проверку инвариантов, не задумываясь, куда что логируется и выводится.

    2. Очень часто приходим к тому, что простой остановки программы недостаточно, а нужно куда-то сохранить информацию об ассерте, то есть стандартный assert не подходит, нужен свой макрос.

    3. Отключение ассертов в релизе тоже не очевидная необходимость. Например, в Chromium недавно стали уходить от debug-only проверок. Поверьте, производительность им тоже важна, но, видимо, баланс пользы сложился в сторону проверок.


    1. viordash Автор
      03.11.2025 10:42

      в Chromium недавно стали уходить от debug-only проверок

      можете указать, где почитать про это, интересны мотивы?
      Быстрый поиск не нашел по этой теме ничего толкового.



  1. Goron_Dekar
    03.11.2025 10:42

    Ни разу не сталкивался с проектом, где мог бы работать стандартный assert. Очень часто ошибку надо не только поймать, но и о ней всем и правильно сообщить.

    Лучшее применение ассертам - контракт. Но в плюсах пока нормальных контрактов не завезли, и если хотите их реализовывать - проще передавать уже валидированные (желательно в компилтайме) объекты, чем валидировать по месту приёма.


  1. vadimr
    03.11.2025 10:42

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


    1. bogolt
      03.11.2025 10:42

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


      1. vadimr
        03.11.2025 10:42

        И кстати не раз видел вываливающиеся ассерты в разном софте.

        В джавовском софте обычно это можно видеть :) Но при этом, как правило, программы продолжают работу, в отличие от описанной в статье логики.


    1. viordash Автор
      03.11.2025 10:42

      при ошибках программиста

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


      1. vadimr
        03.11.2025 10:42

        Если программа находится перед программистом, то проще упасть и исправить. А если управляет каким-то HA процессом (для примера, подачей топлива в двигатель автомобиля), то совсем неправильно падать, а надо предусматривать код для парирования ошибок программиста.

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


        1. viordash Автор
          03.11.2025 10:42

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


          1. vadimr
            03.11.2025 10:42

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


            1. apevzner
              03.11.2025 10:42

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


        1. apevzner
          03.11.2025 10:42

          Как вариант, критически важная программа на assert-е может упасть, а внешняя по отношению к ней система запустить вместо неё аварийную замену (или просто перезапустить её. Или перезапустить несколько раз, а если не помогло - перезапустить аварийную замену).

          Что безопаснее, при обнаружении внутренних ошибок - проглотить их и сделать вид, что всё ОК, когда на самом деле не ОК, или явно раскрутить цепочку обработки отказа?


          1. vadimr
            03.11.2025 10:42

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


            1. apevzner
              03.11.2025 10:42

              Это могут быть логи или core dump.


    1. apevzner
      03.11.2025 10:42

      Это эквивалент kernel panic на Linux или BSOD на венде.

      А вы бы хотели, чтобы вместо этого система продолжила работать, но делала бы что-нибудь нввидимо и неправильно? Например, какая-нибудь важная ядерная нитка подвисла бы навечно и перестала бы отрабатывать относящиеся к ней запросы. И в результате, например, система делала бы вид, что работает, но записи на диск накапливались бы в памяти, а на диск бы не попадали.

      Может всё же в безнадёжной ситуации лучше тогось, чем чтобы программа прикидывалась живой?


      1. vadimr
        03.11.2025 10:42

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


        1. apevzner
          03.11.2025 10:42

          Ошибки ошибкам рознь.

          Если это какие-то неправильные данные, пришедшие снаружи, некорректное или неожиданное поведение аппаратуры, нехватка каких-то ресурсов, типа памяти и т.п., то для ядра это - нормальные, штатные ситуации, которые ядро должно корректно отрабатывать.

          Но если ошибка именно во внутренней логике кода, как вы прикажете её отрабатывать?


  1. aamonster
    03.11.2025 10:42

    Если программист пишет if, а не assert – скорей всего, у него есть на это причины. Например, сегодня переменная точно инициализируется в том же модуле, а завтра может и нет – в зависимости от какого-то неочевидного условия, и при проверке на дебаг-версии на это не наткнулись, а в релизе выстрелит.

    Assert – пометки "для себя", на совсем уж очевидные случаи. If – уже более серьёзная проверка. Ну и третий случай – if, который уберёт компилятор (ибо увидит, что 5 строчками выше переменной присваивается значение).


  1. S1onnach
    03.11.2025 10:42

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