Pull to refresh

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

Reading time3 min
Views14K
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 Добавил подсветку кода.
Tags:
Hubs:
+34
Comments13

Articles