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