7.17 – assert и static_assert

Добавлено 1 июня 2021 в 00:49

В функции, которая принимает параметры, вызывающая функция может передавать аргументы, которые синтаксически верны, но семантически бессмысленны. Например, в предыдущем уроке (7.15 – Обнаружение и обработка ошибок) мы показали следующий пример функции:

void printDivision(int x, int y)
{
    if (y != 0)
        std::cout << static_cast<double>(x) / y;
    else
        std::cerr << "Error: Could not divide by zero\n";
}

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

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

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

Если программа завершится (через std::exit), мы потеряем наш стек вызовов и любую отладочную информацию, которая может помочь нам локализовать проблему. std::abort - лучший вариант для таких случаев, поскольку разработчику обычно предоставляется возможность начать отладку в точке, где программа была прервана.

Предусловия, инварианты и постусловия

В программировании предусловие – это любое условие, которое всегда должно выполняться до выполнения компонента кода. Наша проверка y является предусловием, которое перед продолжением функции гарантирует, что y имеет допустимое значение.

Чаще всего функции с предусловиями записываются следующим образом:

void printDivision(int x, int y)
{
    if (y == 0)
    {
        std::cerr << "Error: Could not divide by zero\n";
        return;
    }
 
    std::cout << static_cast<double>(x) / y;
}

Инвариант – это условие, которое должно выполняться во время выполнения какого-либо компонента.

Точно так же постусловие – это то, что должно выполняться после выполнения какого-либо компонента кода. У нашей функции нет постусловий.

Утверждения

Использование условного оператора для обнаружения недопустимого параметра (или для проверки некоторого другого предположения), наряду с выводом сообщения об ошибке и завершением программы, является настолько распространенным решением задачи, что C++ предоставляет для этого быстрый метод.

Утверждение – это выражение, которое будет истинным, если в программе нет ошибки. Если выражение истинно, утверждение ничего не делает. Если условное выражение вычисляется как ложное, отображается сообщение об ошибке и программа завершается (через std::abort). Это сообщение об ошибке обычно содержит выражение, которое не прошло, в виде текста, а также имя файла исходного кода и номер строки с утверждением. Это позволяет очень легко определить не только причину проблемы, но и ее место в коде, что может очень помочь при отладке.

В C++ утверждения времени выполнения реализуются с помощью макроса препроцессора assert, который находится в заголовке <cassert>.

#include <cassert> // для assert()
#include <cmath>   // для std::sqrt
#include <iostream>
 
double calculateTimeUntilObjectHitsGround(double initialHeight, double gravity)
{
  assert(gravity > 0.0); // Объект не достигнет Земли, если не будет положительной силы тяжести.
 
  if (initialHeight <= 0.0)
  {
    // Объект уже на поверхности Земли. Или под ней.
    return 0.0;
  }
 
  return std::sqrt((2.0 * initialHeight) / gravity);
}
 
int main()
{
  std::cout << "Took " << calculateTimeUntilObjectHitsGround(100.0, -9.8) << " second(s)\n";
 
  return 0;
}

Когда программа вызывает calculateTimeUntilObjectHitsGround(100.0, -9.8), assert (gravity > 0.0) будет вычисляться как false, что приведет к срабатыванию assert. И это напечатает сообщение, подобное следующему:

dropsimulator: src/main.cpp:6: double calculateTimeUntilObjectHitsGround(double, double): Assertion `gravity > 0.0' failed.

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

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

Хотя мы ранее говорили вам избегать макросов препроцессора, утверждения являются одним из немногих макросов, которые считаются приемлемыми для использования. Мы рекомендуем вам свободно использовать инструкции assert во всем вашем коде.

Сделайте ваши инструкции assert более наглядными

Иногда выражения assert не очень информативны. Рассмотрим следующую инструкцию:

assert(found);

Если этот assert сработает, то он скажет:

Assertion failed: found, file C:\\VCProjects\\Test.cpp, line 34

Что это вообще значит? Ясно, что что-то не нашли, но что? Вам нужно будет посмотреть на код, чтобы определить это.

К счастью, есть небольшая хитрость, с помощью которой вы можете сделать свои инструкции assert более наглядными. Просто добавьте с помощью логического И строковый литерал:

assert(found && "Car could not be found in database");

Почему это работает: строковый литерал всегда имеет логическое значение true. Итак, если found равен false, false && true равно false. Если found равен true, true && true равно true. Таким образом, логическое И со строковым литералом не влияет на результат вычисления assert.

Однако при срабатывании assert строковый литерал будет включен в сообщение assert:

Assertion failed: found && "Car could not be found in database", file C:\\VCProjects\\Test.cpp, line 34

Это дает вам дополнительный контекст относительно того, что пошло не так.

NDEBUG

Макрос assert требует небольших накладных расходов, которые возникают при каждой проверке условия assert. Кроме того, assert (в идеале) никогда не должны встречаться в рабочем коде (потому что ваш код уже должен быть тщательно протестирован). Следовательно, многие разработчики предпочитают, чтобы утверждения assert были активны только в отладочных сборках. В C++ есть способ отключить утверждения в рабочем коде. Если макрос NDEBUG определен, макрос assert отключается.

Некоторые IDE устанавливают NDEBUG по умолчанию как часть настроек проекта для конфигураций выпуска. Например, в Visual Studio на уровне проекта устанавливаются следующие определения препроцессора: WIN32; NDEBUG; _CONSOLE. Если вы используете Visual Studio и хотите, чтобы ваши утверждения запускались в сборках выпуска, вам необходимо удалить NDEBUG из этого параметра.

Если вы используете IDE или систему сборки, которая автоматически не определяет NDEBUG в конфигурации выпуска, добавьте его в настройки проекта или компиляции вручную.

Некоторые ограничения и предупреждения об утверждениях assert

У assert есть несколько подводных камней и ограничений. Во-первых, в самом assert может быть ошибка. Если это произойдет, assert либо сообщит об ошибке там, где ее нет, либо не сможет сообщить об ошибке, если она существует.

Во-вторых, ваши утверждения assert не должны иметь побочных эффектов, то есть программа должна работать одинаково с assert и без них. В противном случае то, что вы тестируете в конфигурации отладки, не будет таким же, как в конфигурации выпуска (при условии, что вы для релиза включаете NDEBUG).

Также обратите внимание, что функция abort() немедленно завершает программу, без возможности выполнить дальнейшую очистку (например, закрыть файл или базу данных). Из-за этого assert следует использовать только в тех случаях, когда маловероятно, что произойдет какое-то повреждение, если программа неожиданно завершит свою работу.

assert и обработка ошибок

Утверждения assert и обработка ошибок достаточно похожи, чтобы их цели можно было спутать, поэтому давайте проясним:

Цель утверждения assert – отловить ошибки программирования путем документирования того, чего никогда не должно происходить. Если это действительно происходит, значит, программист где-то допустил ошибку, и эту ошибку можно определить и исправить. Утверждения assert не позволяют восстанавливаться после ошибок (в конце концов, если что-то никогда не произойдет, нет необходимости восстанавливаться после этого), и программа не выдаст дружественного сообщения об ошибке.

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

Лучшая практика


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

Утверждения assert также иногда используются для документирования случаев, которые не были реализованы, потому что они были не нужны в то время, когда программист писал код:

assert(moved && "Need to handle case where student was just moved to another classroom");

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

static_assert

В C++11 добавлен еще один тип утверждения, называемый static_assert. static_assert – это утверждение, которое проверяется во время компиляции, а не во время выполнения, и ошибка static_assert вызывает ошибку компиляции. В отличие от assert, который объявлен в заголовке <cassert>, static_assert является ключевым словом, поэтому для его использования не нужно включать дополнительный заголовочный файл.

static_assert принимает следующую форму:

static_assert(условие, диагностическое_сообщение)

Если условие не равно true, выводится диагностическое сообщение. Вот пример использования static_assert для проверки, что типы имеют определенный размер:

static_assert(sizeof(long) == 8, "long must be 8 bytes");
static_assert(sizeof(int) == 4, "int must be 4 bytes");
 
int main()
{
	return 0;
} 

На машине автора при компиляции компилятор выдал следующую ошибку:

1>c:\consoleapplication1\main.cpp(19): error C2338: long must be 8 bytes

Поскольку static_assert вычисляется компилятором, условие должно быть возможно вычислить во время компиляции. Кроме того, в отличие от обычного assert (который вычисляется во время выполнения), static_assert можно разместить в любом месте файла исходного кода (даже в глобальном пространстве).

В C++11 и C++14 диагностическое сообщение должно быть указано вторым параметром. Начиная с C++17, предоставление диагностического сообщения необязательно.

Теги

assertC++ / CppLearnCppstatic_assertДля начинающихОбнаружение ошибокОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.