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

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)