C++ / [Из песочницы] Compile-time проверка в C/C++

в 13:56, , рубрики: assertion, c plus plus, compile-time computation, метки: , , ,

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,
EM_ItemsCount
};

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

Теперь везде, где используются константы из этого набора, нужно просто добавить проверку:
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 );

switch

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

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

}

Дополнительный бастион для обороны от ошибок. Если действия обрабатываются одинаково, лучше перечислить несколько 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

Что в итоге?
Плюсы:

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

Минусы:

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

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

Автор: to_climb


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js