Compile-time проверка в C/C++

C/C++ позволяют выполнить проверки константных выражений ещё на этапе компиляции программы. Это дешёвый способ избежать проблем при модификации кода в будущем.
Я рассмотрю работу с:
  • перечислениями (enum),
  • массивами (их синхронизацию с enum),
  • switch-конструкциями,
  • а так же работу с классами, содержащими разнородные данные.



BOOST_STATIC_ASSERT и все-все-все


Существуют много способов сломать компилятор во время компиляции. Из них мне больше всего нравится такое исполнение:
#define ASSERT(cond) typedef int foo[(cond) ? 1 : -1]


Но если у вас в программе используется boost, то ничего изобретать не нужно: BOOST_STATIC_ASSERT. Также поддержка обещает быть в С++11 (static_assert).

С инструментом разобрались, теперь об использовании.

Контроль количества элементов в enum


Перечисления — набор связанных по смыслу констант, которые, как правило, используются в точке ветвления логики программы. Точек ветвления обычно несколько, и можно легко что-нибудь пропустить.
Пример:

enum TEncryptMode {
	EM_None = 0,
	EM_AES128,
	EM_AES256,
	<b>EM_ItemsCount</b>
};

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

Теперь везде, где используются константы из этого набора, нужно просто добавить проверку:
ASSERT(EM_ItemsCount == 3);

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

В качестве бонуса от введения EM_ItemsCount появляется возможность вставлять runtime-проверки параметров функции:
assert( 0 <= mode && mode < EM_ItemsCount );

Сравните с вариантом без такой константы:
assert( 0 <= mode && mode <= EM_AES256 );

(добавляем EM_AES512 и получает неправильную проверку)

Массивы и enum


Частный случай проверки из предыдущего раздела.
Предположим, у нас есть массив с параметрами к тем же алгоритмам шифрования (пример немного высосан из пальца, но в жизни встречаются похожие случаи):
static const ParamStruct params[] = {
{ EM_None, 0, ... },
{ EM_AES128, 128, ... },
{ EM_AES256, 256, ... },
{ -1, 0, ... }
};

Требуется поддерживать эту структуру синхронной с TEncryptMode.
(Зачем нужен последний элемент массива, думаю, объяснять не нужно.)

Нам понадобится вспомогательный макрос для вычисления длины массива:
#define lengthof(x) (sizeof(x) / sizeof((x)[0]))

Теперь, можно записать проверку (лучше, если сразу за определением params):
ASSERT( lengthof(params) == EM_ItemsCount + 1 );

upd: В комментариях хабраюзер skor предложил более безопасный вариант макроса lengthof, за что ему спасибо.

switch


Тут всё очевидно (после примеров выше). Перед switch(mode) добавляем:
ASSERT(EM_ItemsCount == 3);


Чуть менее очевидная runtime-проверка:
ASSERT(EM_ItemsCount == 3);
switch( mode ) {
case ...: ... break;
...
<b>default:
assert( false );</b>
}

Дополнительный бастион для обороны от ошибок. Если действия обрабатываются одинаково, лучше перечислить несколько case-условий для одного действия, оставив default не занятым:
...
case ET_AES128:
case ET_AES256:
...
break;
...


Классы с разнородными данными


Отвлечёмся от enum'ов и посмотрим на такой класс:
class MyData {
...
private:
int a;
double b;
...
};


Очень может быть, что когда-то в будущем кто-то захочет добавить в него переменную int c. Класс к этому времени стал большим и сложным. Как найти точки, в которые нужно прописать переменную c?

Предлагается такой полуавтоматический способ решения — заводим в классе константу версии данных:
class MyData {
static const int DataVersion = 0;
...
};

Теперь во всех методах, в которых важно отследить целостность всех данных, можно прописать:
ASSERT(DataVersion == 0);


Добавляя новые данные в класс, придётся вручную увеличить константу DataVersion (тут требуется дисциплина, увы). Зато компилятор сразу обратит внимание на те места, которые нужно проверить. К таким точкам проверки должны относиться:
  • конструкторы,
  • оператор присваивания (operator=)
  • операторы сравнения (==, <, etc),
  • чтение/запись данных (в том числе <<, >>),
  • деструктор (если он не тривиальный).

Остальные места проверки зависят от внутренней логики (вывод в лог, например).
Эту же константу (DataVersion) удобно использовать при сохранении данных на диск (если интересно, могу написать об этом отдельно).

Benefit


Что в итоге?
Плюсы:
  • Автоматическая проверка целостности на этапе компиляции (порой, это экономит часы и даже дни отладки).
  • Нулевые накладные расходы на этапе выполнения.


Минусы:
  • Дополнительный код (хоть и относительно небольшой).
  • Нагрузка на самодисциплину (нужно именно просмотреть сработавшие падения, а не просто поправить константу).


Для меня плюсы перевешивают, а для вас?

upd Добавил подсветку кода.
Поделиться публикацией
Похожие публикации
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 13
  • +1
    Статическая проверка очень полезна, я только за её использование. Сам использую реализацию от Александреску.
    • 0
      Способ Александреску идет в контексте использования шаблонов, как пример реализации static_assert с их помощью. Способ автора лично мне кажется более наглядным.
    • 0
      Макросы это не проверка на этапе компиляции. Это проверка на этапе препроцессинга. Любой человек который работал с макросами в большом проекте знает какой это геморрой. Я тут имею в виду не потенциальную опасность макросов (про это столько уже сказано что повторяться нет смысла), я имею в виду дебаг.

      Иными словами, зачем писать макрос assert если есть буст? Там кстати макрос лишь обертка для проверки темплейтом. Если в проекте нет буста (и нет С++11) темплейт можно написать самому — там всего-то две строчки.
      • +2
        Честно говоря, не вижу разницы, каким способом заставить компилятор выдать ошибку (boost и новый стандарт упомянуты).
        Способ с шаблонами описан по ссылке на stackoverflow, но выглядит менее лаконичным в применении, имхо.
      • +1
        Предлагается такой полуавтоматический способ решения — заводим в классе константу версии данных:
        class MyData {
        static const int DataVersion = 0;

        };
        Теперь во всех методах, в которых важно отследить целостность всех данных, можно прописать:
        ASSERT(DataVersion == 0);

        Как вариант вместо DataVersion использовать sizeof(MyData). Получается автоматически, но может не отловить одновременное удаление старых и добавление новых членов данных с одинаковым суммарным размером.
        • 0
          А ещё выравнивание, а ещё unicode/ansii и прочие «ой, добавили виртуальный метод». Я бы не стал так делать, это перегиб в другую сторону — менять константу придётся чаще и не всегда по делу (при портировании, например).

          По поводу «добавили+удалили» — такие же изменения в enum'е можно и не отловить, лучше просто разносить правки (добавили что-то, собрали, исправили, удалили что-то и т.д.).
        • +1
          Вы правильно написали, что default лучше не использовать, так как при добавлении нового значения в enum компилятор(gcc) выдаёт предупреждение, что новое значение не используется в switch-case, многие это упускают из виду.
          • 0
            Ещё хотелось бы напомнить, что с недавнего времени модно длину массивов стало определять через хромовский макрос:
            template <typename T, size_t N>
            char (&ArraySizeHelper(T (&array)[N]))[N];
            #define ARRAY_SIZE(array) (sizeof(ArraySizeHelper(array)))

            или в более читабельном варианте:
            template <typename T, size_t N>
            typename boost::add_reference<char[N]>::type ArraySizeHelper( T (&)[N] ) ;
            #define ARRAY_SIZE(array) (sizeof(ArraySizeHelper(array)))

            это исключает возможность использования макроса для произвольного указателя.
            • 0
              Спасибо, полезное замечание.
              Обновил пост и попробую внедрить такой вариант у себя.
              • 0
                Примерно такая же реализация макроса _countof в Visual C++ (http://msdn.microsoft.com/en-us/library/ms175773(v=vs.80).aspx)
              • –4
                Какие костыли приходится придумывать, когда нет нормальной среды.
                • +2
                  Зато этот «костыль» будет работать на всех компиляторах и, тем более, средах.
                • 0
                  Вот здесь, кратко про static_assert в C++11, штука однозначно полезная!

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