Все помнят как в школе при решении задач обязательно было делать проверку?
Рекомендуемая литература:
- Modern C++ Design, Andrei Alexandrescu
- Boost.StaticAssert
- Рекомендуемый к прохождению тест - Тест знаний C++ Templates
Проверки
В своей прошлой статье про перечисления я писал что оправданием для того чтобы записывать отображение для элементов перечисления вручную может быть только обязательное наличие статической проверки на размер перечисления и массива отображения. Вспомним, у нас было перечисление типа:
enum color{ RED, GREEN, BLUE };
а также массив строковых представлений для каждого элемента перечисления, внутри некоторой функции отображения:
const char* mapping(color c)
{
const char* const strs[]={"Red as an egypt rose", "Green peace", "Blue Screen of Death"};
return strs[c];
}
Собственно всё достаточно просто и понятно.... вам понятно. А теперь представьте себе, что другой человек добавил в перечисление новый элемент:
enum color{ RED, GREEN, BLUE, BRIGHTNESS };
Внимание вопрос:
- Откуда этому человеку знать о вашей функии mapping(), которая объявлена в другом модуле?
- Сколько времени вы потратите всей конторой, когда будете искать почему у заказчика падает сценарий не покрытый вашими тестами, который всего лишь использует этот новый член перечисления при вызове функции
mapping()?
Как быть? Всё достаточно просто - нужно всего лишь проверять, что размер массива отображений подходит вашему перечислению. Т.е. в перечисление нужно добавить фиктивный член который будет замыкать перечисление:
enum color{ RED, GREEN, BLUE, BRIGHTNESS, COLOR_END };
А в функцию - соответствующую проверку:
const char* mapping(color c)
{
const char* const strs[]={"Red as an egypt rose", "Green peace",
"Blue Screen of Death"};
if(sizeof(strs)/sizeof(strs[0]) != COLOR_END)
throw std::runtime_error("Color mapping table length mismatch");
return strs[c];
}
Теперь вы хотя бы увидите meaningful сообщение об ошибке в логах своей программы и поиск ошибки займёт не так много времени, к тому же если у вас есть хоть один тест использующий эту функцию, то он выявит ошибку. Но это всё - всё равно требует тестирования. Можно ли сделать так чтобы ошибка не вышла за пределы компьютера отдельно взятого программиста, даже без покрытия этой функии тестами? Можно - об этом и поговорим.
Статические проверки без использования возможностей C++
Рассмотрим ещё раз то условие, которое мы проверяем:
sizeof(strs)/sizeof(strs[0]) != COLOR_END
Слева стоит арифметическое выражение численные значения операндов которого известны в момент компиляции программы, соответственно и численное значение выражения слева известно в момент компиляции. Ну а справа - просто константа, значение которой также известно в момент компиляции. Таким образом и результат проверки известен ещё во время компиляции, а значит и проверку можно делать во время компиляции и даже попытаться запретить успешно компилировать программу если проверка не пройдена.
Достаточно добавить в код нечто что будет компилироваться если условия равенства выполняется и не будет компилироваться в противном случае. Например вот это:
const char* mapping(color c)
{
const char* const strs[]={"Red as an egypt rose", "Green peace", "Blue Screen of Death"};
char ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH[ sizeof(strs)/sizeof(strs[0]) == COLOR_END ];
return strs[c];
}
Таким образом если значение COLOR_END отличается от количества элементов в массиве - выражение в скобках будет ложным, т.е. интепретировано как ноль и компилятор выдаст ошибку о том что невозможно создать массив нулевого размера:
Comeau 4.3.10.1
error: the size of an array must be greater than zero
char ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH[sizeof(strs)/sizeof(strs[0]) == COLOR_END];
Visual C++ 2008 Express
error C2466: cannot allocate an array of constant size 0
error C2133: 'ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH' : unknown size
Уверен, многие пользователи GCC скажут:
Что за отстой!? У меня стоит последняя версия ГЦЦ и она это компилирует! Вижуал С++ - не эталон правильности, а о компиляторе Камо я вообще первый раз слышу!
Да всё верно, компилирует, но это - проблема именно ГЦЦ. Стандарт по поводу массивов гласит (переводить не стану, дабы меня не упрекнули в неточном переводе):§8.3.4
In a declaration T D where D has the form
D1 [constant-expressionopt]
and the type of the identifier in the declaration T D1 is “derived-declarator-type-list T,” then the type of the
identifier of D is an array type. T is called the array element type; this type shall not be a reference type, the
(possibly cv-qualified) type void, a function type or an abstract class type. If the constant-expression
(5.19) is present, it shall be an integral constant expression and its value shall be greater than zero.
На самом деле - не беда, обойти эту вольность в компиляторе можно достаточно просто, нужно просто создавать массив не нулевого, а отрицательного размера (или же указывать для ГЦЦ параметр компиляции -pedantic-errors):
char ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH[ sizeof(strs)/sizeof(strs[0]) == COLOR_END ? 1 : -1 ];
Тогда мы получаем следующий результат:
Comeau 4.3.10.1
error: the size of an array must be greater than zero
char ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH[sizeof(strs)/sizeof(strs[0]) == COLOR_END ? 1 : -1 ];
Visual C++ 2008 Express
error C2118: negative subscript
GCC 4.3.0 (MinGW)
In function 'const char* mapping(color)':
error: size of array 'ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH' is negative
Как видите теперь это работает и под ГЦЦ, но для Вижуал С++ название переменной (которое содержит описание ошибки) теперь не выводится. У данного варианта статической проверки с использованием массивов есть ещё один минус - когда проверка проходит успешно, вы получаете предупреждение от комилятора о том что переменная была объявлена, но никогда не используется. Это предупреждение устраняется довольно просто - нужно всего лишь декларировать не массив, а тип:
typedef char ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH[ sizeof(strs)/sizeof(strs[0]) == COLOR_END ? 1 : -1 ];
Итого имеем отклонение от идеального поведения (потеря названия или же предупреждение) для одного компилятора из трёх тестируемых. Результат не плохой, но, например, мне использовать Камо практически не приходится, а вот Вижуал С++ я использую не реже ГЦЦ, поэтому двигаемся дальше. Для тех кто работает в чистом Си и хочет использовать подобные проверки, способ описанный выше наверное единственный (другого я не знаю).
Статические проверки
Хватит впрочем ходить вокруг да около - выше я указал какими путями мы шли к решению, и недостатки тех или иных решений. Сейчас просто покажу решение которое использую сейчас. Принцип действия схож описанному Александреску в Совеременном программировании на С++, но отличается тем, что может быть использовано в глобальной области видимости, т.е. может быть использовано вне функций, а так же генерирует (псевдо)уникальные имена, что позволяет не задумываться о том что имена классов проверок могут совпасть и вызвать ошибку компиляции. Для универсальности определим макрос, который будет принимать в себя проверяемое выражение и сообщение об ошибке:
#include <boost/preprocessor/cat.hpp>
namespace static_check{
template<bool>struct check{inline check(){}};
template<>struct check<false>;
}
#define STATIC_CHECK(expr,msg) \
static_check::check<(expr!=0)> \
BOOST_PP_CAT(BOOST_PP_CAT(ERROR__,msg),BOOST_PP_CAT(_at_line_,__LINE__))
Теперь проверка в функции выглядит так:
const char* mapping(color c)
{
const char* const strs[]={"Red as an egypt rose", "Green peace", "Blue Screen of Death"};
STATIC_CHECK(sizeof(strs)/sizeof(strs[0]) == COLOR_END, ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH);
return strs[c];
}
Тогда сообщения об ошибке будут следующими:
Comeau 4.3.10.1
error: incomplete type is not allowed
STATIC_CHECK(sizeof(strs)/sizeof(strs[0]) == COLOR_END, ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH);
Visual C++ 2008 Express
error C2079: 'ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH_at_line_14' uses undefined struct
'static_check::check<false>'
GCC 4.3.0 (MinGW)
In function 'const char* mapping(color)':
error: aggregate 'static_check::check<false> ERROR__ENUM_COLOR_AND_ARRAY_SIZE_MISMATCH_at_line_14' has incomplete
type and cannot be defined
Итак... что же собственно происходит? Ничего особенного - если условие выполняется, то мы просто создаём переменную типа check<true>, в противном же случае - переменную незавершённого типа check<false>, которая создана быть не может.
Куда же делось предупреждение о том что эта самая переменная создаётся, но не используется? Да никуда, просто переменная используется, пускай и только в момент конструирования объекта, когда вызывается нетривиальный конструктор по умолчанию (да, для компилятора он нетривиален).
Велосипед
Всё уже украдено, до нас (с) Операция Ы
... который был опять изобретён. Если вам надо просто производить статическую ошибку, не обязательно выводить её описание и вы не любите чужие велосипеды - сделайте проще:
#include <boost/static_assert.hpp>
const char* mapping(color c)
{
const char* const strs[]={"Red as an egypt rose", "Green peace", "Blue Screen of Death"};
BOOST_STATIC_ASSERT(sizeof(strs)/sizeof(strs[0]) == COLOR_END);
return strs[c];
}
В библиотеке буст механизм статических проверок уже реализован, и может и нет смысла что-то ещё изобретать. Повторюсь - я сторонник вывода информации об ошибке, поэтому и изобрёл свой велосипед.
Собственно, я закончил. Что хотел - то рассказал, кому надо было - тот понял. Рацпредложения по улучшению - велкам.
----
Дмитрий analizer Потапов, март-апрель 2009
Если дважды использовать предлагаемый велосипед с одним и тем же сообщением в разных заголовочных файлах в одном и том же пространстве имён в строках с одним и тем же номером и оба заголовочных файла поместить в одну единицу трансляции, то будет конфликт имён. Если его использовать внутри класса, то размер класса увеличится за счёт нигде не используемой переменной.
Помимо BOOST_STATIC_ASSERT в Boost есть ещё и BOOST_MPL_ASSERT_MSG (см. http://www.boost.org/doc/libs/1_39_0/libs/mpl/doc/refmanual/assert-msg.html ), который обеспечивает вывод компилятором пользовательского сообщения.
Признаю что мой макрос не "хороший", но ситуация с совпадением имён и строк довольно редка, в остальном - согласен.
Не совсем. Мы формируем локальный массив из указателей на строковые константы которые уже лежат в области данных и возвращаем один из этих указателей, который не меняется и является валидным на всём протяжении работы программы.
Если смущает - можно ещё слово static перед декларацией переменной добавить, типа даже ускорение сможем получить... наверное.