Решение проблемы - собираем велосипед
Решение без велосипедов - использование BOOST C++
Второе решение - инструменты библиотеки Loki
Все C++ программисты так или иначе сталкивались со стандартным макросом assert, пришедшим из C. В отладочной версии программы он во время исполнения проверяет, что условие не равно 0, в противном случае выдает сообщение об ошибке. Вот простой пример его использования, через который мы плавно перейдем к изложению цели наших посиделок за консолью.
Допустим, есть алгоритм, работающий с текстовой информацией. Мы предусмотрели возможность работы с 1- и 2-байтовыми символами (то есть char и wchar_t).
switch( sizeof(T_CHAR) )
{
case 1: ... break;
case 2: ... break;
default: assert(0);
}
Любой опытный (то есть не раз наступивший на грабли языка C++) программист почти обязательно впишет обработку ветки default в операторе switch, чтобы не получить сюрприз. Кстати, компилятор gcc (и MSVS 2003 тоже) в этом смысле умен - он выдает предупреждение, что не все альтернативы условия в операторе switch обработаны. Но так бывает только если альтернативы известны (условие типа enum).
Казалось бы, все замечательно - непредусмотренный алгоритмом случай вызовет ошибку при исполнении..
Но может и не вызвать - в релиз-версии программы, когда макрос assert отключен. Кроме того, даже в отладочной версии можно никогда не наткнуться на проверку - просто в силу недостаточного подбора тестовых случаев.
Был у нас как раз такой случай. Windows-версия программы работала на ура, а Linux-версия сбоила, причем делала это редко (что еще хуже). Причиной оказалось то, что тип wchar_t компилятором gcc реализован как 4-байтовый. А часть кода, работавшая лишь при некоторых сочетаниях условий, была привязана к соблюдению условия, что размер символов равен либо 1 байт, либо 2 байта.
Можно ли отловить случаи, когда алгоритм будет работать некорректно, не выполняя тестовые прогонки программы? Для этого нужно, чтобы во время компиляции программы транслятор умел выполнять проверку условий и выдавать осмысленное описание ошибки при нарушении этого условия. То есть, фактически, нам нужна метапрограмма.
Решение задачи возможно несколькими способами. Покажем первый - с написанием собственной версии основного инструментария, чтобы затем перейти к решениям, предложенному в библиотеках Boost и Loki (эти решения приведены далее).
Во-первых, можно использовать тот факт, что в C/C++ недопустимы массивы нулевой длины. Вдумайтесь в этот факт - проверка на ноль длины массива выполняется компилятором во время трансляции. Это то, что доктор прописал (первое письменное упоминание этого приема датируется хрониками в 1997 году)!
#define STATIC_CHECK(condition) { char dummy[ (condition) ? 1 : 0]; }
Этот макрос проверяет во время компиляции, что его аргумент не равен нулю. К примеру, код
STATIC_CHECK( sizeof(wchar_t)<=2 );
даст ошибку компиляции, если тип wchar_t имеет непредусмотренный нами размер 4 байта.
Это решение работоспособно, но непрактично в том плане, что выдаваемое сообщение об ошибке совершенно не проясняет ситуацию. Вот, к примеру, что выдает MS VisualStudio 2005:

Согласитесь, не совсем информативно, хотя - работает! В принципе, общая беда любого кода с участием шаблонов - невнятные ошибки, но в данном случае есть возможность облагородить в некоторых пределах наш инструмент.
Скачать файл compile-time-assertions.cpp (300 байтов)
Теперь посмотрим, как можно улучшить юзабилити нашего макроса.
Решение основано на специализации шаблонов. Вот код:
template < bool > struct CompileTimeChecker;
template <>
struct
CompileTimeChecker<true>
{
CompileTimeChecker(...) {};
};
#define STATIC_CHECK( expr ) { sizeof( CompileTimeChecker< (expr) != 0 > ( expr
) ); }
Вся механика здесь построена на специализации шаблона CompileTimeChecker. Обратите внимание, что специализация параметра для false не объявлена - это-то и является второй веткой альтернативы проверки.
Скачать файл compile-time-assertion-2.cpp (353 байта)
Наконец, после рассмотрения двух вариантов реализации проверки условия на стадии компиляции, можно спокойно отказаться от самостоятельно написанного кода и воспользоваться удивительной библиотекой BOOST C++. Среди множества вещей там есть библиотечка Static Assertions - как раз то, что нужно, особенно если учесть, что код BOOST C++ проверяется на множестве платформ (надо заметить, что реализация деталей использования шаблонов сильно различается для разных компиляторов - это может сильно испортить настроение при написании платформонезависимого кода).
#include <boost/static_assert.hpp>
...
BOOST_STATIC_ASSERT( sizeof(wchar_t)
<=2 );
|
Классика жанра: книга одного из основоположников современного подхода к метапрограммированию в языке C++ |
Теперь посмотрим на другую замечательную библиотеку - Loki. Ее автор предлагает рассмотреть другую задачу, где необходима проверка во время компиляции, и мы последуем за ним.
Предположим, что мы разрабатываем функцию, которая выполняет безопасное преобразование типа (см. для справки библиотеку Boost.Conversion). Задача заключается в том, чтобы гарантировать, что в ходе преобразования нет потери информации - как например в коде
double a=M_PI;
float b = static_cast<float>(a);
Самый простой и общий способ проверить это - сравнить размер соответствующих типов, хотя такой подход отсекает значительное число корректных случаев (если записать в переменную double значение 1, то можно спокойно преобразовывать ее в типы float, int и даже char - потери разрядов не будет!).
Пишем первый вариант реализации функции:
template <class To,
class From>
To safe_reinterpret_cast( From from )
{
assert(sizeof(From) <=
sizeof(To) );
return reinterpret_cast<To>(from);
}
Использовать ее можно так же, как и стандартные операторы преобразования C++:
int i = 123456;
char *bad_pointer = safe_reinterpret_cast<char*>(i);
Тип To, к которому выполняется приведение, указывается явно, а исходный тип From выводится компилятором из аргумента вызова шаблонной функции. Что же нас не устраивает в данной версии функции? Самое главное - потенциально неверные преобразования обнаруживаются только при работе скомпилированной программы и только в ее отладочной версии (в релиз-версии обычно макрос assert() отключается). То есть ошибка не будет обнаружена до момента, пока соответствующий участок программы не будет исполнен. Но обратите внимание, что вся информация о типах в данном случае известна компилятора при конкретизации шаблонной функции safe_reinterpret_cast! Так почему бы не заставить компилятор выполнять проверку размеров типов при трансляции?
Вот новая реализация функции:
#define STATIC_CHECK(expr) { char dummy[ (expr) ? 1 : 0 ]; }
template <class To,
class From>
To safe_reinterpret_cast( From from )
{
STATIC_CHECK( sizeof(From) <=
sizeof(To) );
return reinterpret_cast<To>(from);
}
По сути, функционально это вполне работоспособный код, в котором во время компиляции с помощью вспомогательного макроса STATIC_CHECK проверяется условие - размер нового типа To должен быть не меньше размера старого типа From. Его недостаток - непонятное сообщение об ошибке, генерируемое в случае несоблюдения условия безопасного преобразования. Действительно, сообщение о том, что размер массива dummy задан как 0 (см. скриншот выше) может сбить с толку.
Единственный способ заставить компилятор выдать сообщение об ошибке с "заказанным" нами содержимым - объявить переменную или класс с таким именем (пробелы и многие другие символы использовать конечно нельзя - только подчеркивания, цифры в первой позиции тоже недопустимы) и намеренно сделать синтаксическую ошибку, чтобы компилятор вывел сообщение об ошибке и впечатал туда имя переменной или класса (такие вещи совершенно не стандартизированы, и остается только полагаться на мэйнстрим в индустрии компиляторов - большинство современных компиляторов имеет необходимое нам поведение).
Попробуем модифицировать макрос STATIC_ASSERT, введя в него второй аргумент - выдаваемое сообщение при нарушении условия.
template<bool>
struct
CompileTimeChecker
{
CompileTimeChecker(...);
};
template<> struct CompileTimeChecker<false> {};
#define
STATIC_CHECK(expr,msg) \
{ \
class ERROR_##msg {};\
(void)sizeof(
CompilerTimeChecker<(expr)!=0>((ERROR_##msg())));\
}
Теперь функцию безопасного преобразования можно реализовать так:
template < class
To, class From >
To safe_reinterpret_cast( From from )
{
STATIC_CHECK( sizeof(From) <=
sizeof(To), Destination_Type_Too_Narrow );
return
reinterpret_cast<To>(from);
}
Что интересного можно обнаружить в этом коде? Во-первых, частичную специализацию шаблона (для CompileTimeChecker). Во-вторых, конструктор класса, который принимает любой аргумент - это описывается с помощью декларатора '...'. В третьих, объявление локального (внутри шаблонной функции safe_reinterpret_cast) класса с именем, состоящем из префикса ERROR_ и затем - указанного нами сообщения об ошибке.
Посмотрим, что получится при реальном применении новой версии функции преобразования. Рассмотрим такой код:
void* somePointer = NULL;
char c = safe_reinterpret_cast<char>(somePointer);
После подстановки макроса STATIC_CHECK, компилятор имеет дело с такой реализацией:
template <class
To, class From>
To safe_reinterpret_cast( From from )
{
{
class ERROR_Destination_Type_Too_Narrow
{};
(void)sizeof(
CompileTimeChecker<(sizeof(From) <=
sizeof(To))>(ERROR_Destination_Type_Too_Narrow()));
}
return
reinterpret_cast<To>(from);
}
Если условие безопасности преобразования выполняется (то есть sizeof(From) <= sizeof(To)), то созданный временный объект класса ERROR_Destination_Type_Too_Narrow успешно выступает в роли аргумента при создании объекта конкретизации шаблонного класса CompileTimeChecker<true> - его конструктор принимает любые типы благодаря '...'. Если же условие не соблюдается, то вызвать конструктор конкретизации шаблонного класса CompileTimeChecker<false> не удастся, так как для нее конструктор просто не объявлен, поэтому компилятор выдаст сообщение об ошибке, которое весьма вероятно будет содержать упоминание о невозможности преобразовать объект ERROR_Destination_Type_Too_Narrow в CompileTimeChecker<false>. Для компилятора MS VisualStudio результат такой:

В библиотеке Loki приведенный выше макрос STATIC_CHECK объявлен в хидере static_check.h. Вы можете скачать и скомпилировать программу, использующую описанную технологию проверки условий во время компиляции.
Проект для MS VisualStudio 2003 (2 Kb)
Часть материала данной статьи написана на основе и по мотивам главы 2 книги "Modern C++ Design: Generic Programming and Design Patterns applied" by Andei Alexandrescu (см. описание библиотеки Loki).
Последние изменения 12.06.2005
© Mental Computing 2009