понедельник, 14 июня 2010 г.

static assert & Co

Достаточно недавно получил некоторе количество ехидных комментариев за ручную проверку инвариантов при создании своего шаблона (в критичном по времени выполнения месте внутри цикла я проверял полное совпадение битовых флагов у двух разных структур, чтобы можно было безнаказанно перекинуть набор флагов из одной в другую прямым копированием).
По ходу поговорили и про static_assert'ы, которые я реализовал в этом месте самостоятельно, не подключая тот же boost...

Ехидничать можно сколько угодно, но проблема имеет место быть, хотя отчасти и надуманная.

Старинные рецепты времен C известны достаточно хорошо. Это что-то типа
#define STATIC_ASSERT_CHECK(cond) void static_assert_check(int dummy[(cond) ? 1 : -1])

или
#define STATIC_ASSERT_CHECK(cond) void typedef int static_assert_check[(cond) ? 1 : -1]


NB, обращаю внимание, что скобки вокруг cond критически важны, как и всегда при работе с препроцессором ;-)

Эти решения работают практически в любом месте программы, даже если мы используем C.

Основным недостатком такого метода является невразумительное сообщение об ошибке - что-то типа "error C2118: negative subscript" в случае MSVC.
Codepad ругается "error: size of array 'dummy' is negative, compilation terminated due to -Wfatal-errors", что тоже не намного лучше (кстати, отсутствие fatal-errors привело бы, видимо, к простому предупреждению).

Соответственно, основые усилия народа направлены обычно на увеличение читаемости сообщения об ошибке.

Мгновенный поиск в интернете дает целый набор решений для C++, например, здесь
template  struct STATIC_ASSERTION_FAILURE;
template <> struct STATIC_ASSERTION_FAILURE {};
#define STATIC_CHECK(x) sizeof(STATIC_ASSERTION_FAILURE< (bool)(x) >)


Это не говоря о классических решениях из Мейерса или Александреску.

Кстати, недостатком приведенного выше кода является то, что использовать его можно далеко не везде - например, вне тела функции/метода такой фокус не сработает.

Итак, обычно перед собой ставят две цели:
* Получить универсальный код, который можно использовать везде
* При этом еще и поиметь достаточно внятное сообщение об ошибках, которое хотя бы наводит на мысль о том, что произошло

Решения обычно выглядят как-то так (в частности, в случае с boost static_assert один из вариантов реализации именно такой):

#define STATIC_ASSERT_JOIN2(a,b) a##b
#define STATIC_ASSERT_JOIN(a,b) STATIC_ASSERT_JOIN2(a,b)
template struct static_assert_failure;
template<> struct static_assert_failure {};
#define STATIC_ASSERT(cond) enum { STATIC_ASSERT_JOIN(check, __LINE__) = sizeof(static_assert_failure<(cond)> ) }


При развертывании макроопределения получится такой код:
enum { static_assert_check_line_13_in_file = sizeof(static_assert_failure<(true)> ) };


Альтернативный вариант - использование аналогичного typedef.

Обращаю внимание, что для корректной склейки лексем препроцессором нужно имеенно две фазы STATIC_ASSERT_JOIN - иначе трюк не сработает.

Вроде бы, все хорошо, вот только имеется ряд подводных камней - например, если у нас по несчастливому стечению обстоятельств во включаемом заголовочном файле и собственно .CPP-файле STATIC_ASSERT находится на одной строке и в одном scope'e, получится маловразумительное сообщение об ошибке.

Можно пытаться его как-то сделать более ясным:

#define STATIC_ASSERT_JOIN2(a,b) a##b
#define STATIC_ASSERT_JOIN(a,b) STATIC_ASSERT_JOIN2(a,b)
template struct static_assert_failure;
template<> struct static_assert_failure {};
#define STATIC_ASSERT(cond) enum { JOIN(static_assert_check_line_, JOIN(__LINE__,_in_file) ) = sizeof(static_assert_failure<(cond)> ) }


В этом случае у нас будет более осмысленное сообщение, что-то типа "'static_assert_check_line_13_in_file' : redefinition; previous definition was a 'enumerator'". Короче, легче становится, но не намного...

Особенно забавно, наверное, нарваться на такую ситуацию, когда строки сдвигаются в результате работы, например, beautifier'a, для особой пикантности напускаемого в автоматическом режиме ;-)

BOOST_STATIC_ASSERT для MSVC выходит из положения, задействуя нестандартное расширение Microsoft __COUNTER__ вместо __LINE__, остальным, видимо, предлагается спасаться другими способами, индивидуальными для каждого поддерживаемого компилятора.

Начинающему assert'описателю некоторое количество приятных минут доставит ключик /ZI в MSVC для активации "Program Database for Edit & Continue" (Q199057), по крайне мере мне доставил.

Кстати, испорченный макрос __LINE__ влияет не только на assert'ы... Мы, правда, в production коде как-то /ZI традиционно не используем, поскольку продукт большой, а посему используется специальная система сборки, но в тестовом проекте на это наступить можно.

Если посмотреть в boost/static_assert.hpp, то обнаружится, что есть индивидуальные workaround'ы для шести, как минимум, компиляторов разных версий и производителей, где-то не работает вариант с enum, где-то c typedef, где-то нужно просто заняться черной магией :-)

Вот и вопрос - а стоит ли такими вещами вообще заниматься, бороться с переменным успехом за читаемость, делать workaround'ы для тараканов каждого компилятора?
С учетом того, что сейчас 99 человек из 100 для работы используют какую-нибудь интегрированную среду, она все равно точно спозиционирует разработчика на сломанный STATIC_ASSERT и он
все увидит в исходном коде...

В свете C++0x, где эти assert'ы, кажется, будут, так точно - не стоит.

Комментариев нет: