Шаблонная магия, метафункция IsValidExpression
Доброго времени суток, уважаемое Хабрасообщество.
Сегодня я хочу поделиться одним интересным приемом, который позволяет определять компилируемость любого конкретного выражения.
Пример:
/* Определяем метафункцию HasF, которая позволяет определить наличие функции f() у любого класса. */
DECLARE_IS_VALID_EXPRESSION(
HasF,
( ( U * ) NULL )->f() /* Это выражение компилируемо только если присутствует U::f() */ );
struct Foo{ void f(); };
struct Bar{};
BOOST_STATIC_ASSERT( HasF< A >::value ); /* Тут константа HasF< A >::value будет true */
BOOST_STATIC_ASSERT( !HasF< B >::value ); /* Тут константа HasF< A >::value будет false */
Как Вы уже, наверное, догадались мы будем думать как написать макрос DECLARE_IS_VALID_EXPRESSION.
Итак, наша цель — научиться определять, скомпилируется ли какое-либо выражение или нет. При этом компилятор, естественно, не должен выдавать никаких ошибок: должна просто генерироваться константа, со значением 0, если выражение некомпилируемо, и значением 1 в противном случае.
Релизация
Для этого мы будем использовать принцип SFINAE (substitution failure is not an error). На человеческом языке это означает, что если компилятор встречает «ошибку» внутри определения шаблона (не в теле шаблона, а в определении, т.е. в тех местах, где компилятор старается «подобрать» адекватные коду шаблонные параметры), то эта «ошибка» приводит не к ошибке компиляции, а к прекращению попытки инстанцировать шаблонную функцию (или класс) с теми параметрами, которые вызывают «ошибку».
Именно так работает следующий код:
$define DECLARE_IS_VALID_EXPRESSION( NAME, U_BASED_RUNTIME_EXPRESSION ) \
template< class T > \
struct NAME \
{ \
/* Нам потребуется какой-нибудь тип, который точно не T для сравнения */ \
struct CDummy{}; \
\
/* Эта перегрузка будет работать только когда U_BASED_RUNTIME_EXPRESSION не содержит "ошибок" \
** В противном случае эта перегрузка будет проигнорирована согласно SFINAE. */ \
template< typename U > \
static decltype( U_BASED_RUNTIME_EXPRESSION ) F( void * ); \
\
/* А вот эта перегрузка присутствует всегда, но приоритет ее ниже, потому как троеточие */ \
template< typename U > \
static CDummy F( ... ); \
\
/* Этого typedef могло бы и не быть, но без него этот класс работает неправильно :( \
** (пользуясь случаем передаю привет тестерам комманды Visual Studio) */ \
typedef decltype( F< T >( nullptr ) ) \
TDummy; \
\
enum \
{ \
/* value будет 1, если U_BASED_RUNTIME_EXPRESSION не содержит "ошибок" и 0 в противном случае \
** Почему? \
** Если "ошибок" нету, то присутвуют обе версии F, и F< T >( nullptr ) выбирает ту, \
** в которой нету троеточия, т.е. с нашим тестируемым выражением, а ее возвращаемый тип никак
** не CDummy, т.к. CDummy объявлен локально. \
** Если же "ошибки" есть, то вариант F с тестируемым выражением будет выкинут, и, \
** соответственно, F< T >( nullptr ) выберет вторую перегрузку (которая возвращает CDummy) */ \
value = !boost::is_same< CDummy, TDummy >::value \
}; \
};
Данная реализация, к сожалению, требует наличия C++0x (мой компилятор — VC10). Теоретически возможно обойтись и без нового стандарта (идея та же, но вместо decltype используется sizeof). Но! Здесь я снова хочу передать привет тестерам из Майкрософт, т.к. sizeof работает неправильно в области определения шаблона — он там «не ожидается» (если я правильно помню). В gcc решение на sizeof работает нормально.
Применение
Примером применения может служить, например, следующий код:
/* Определяет метафункцию IsStreamSerializationSupported, которая возвращает
** true, если аргумент поддерживат ввод/вывод через потоки */
DECLARE_IS_VALID_EXPRESSION(
IsStreamSerializationSupported,
( (std::cout << *(U *)NULL), (std::cin >> *(U *)NULL) ) );
/* double поддерживает ввод/вывод через потоки "из коробки" */
BOOST_STATIC_ASSERT( IsStreamSerializationSupported< double >::value );
struct Foo{};
/* А вот Foo ввод/вывод через потоки не поддерживает :( */
BOOST_STATIC_ASSERT( !IsStreamSerializationSupported< Foo >::value );
struct Bar{};
template< class TChar, class Traits >
std::basic_ostream< TChar, Traits > &operator<<(
std::basic_ostream< TChar, Traits > &, const Bar & );
template< class TChar, class Traits >
std::basic_istream< TChar, Traits > &operator>>(
std::basic_istream< TChar, Traits > &, Bar & );
/* Bar поддерживает ввыод/вывод через потоки, т.к. определены соответствующие операторы. */
BOOST_STATIC_ASSERT( IsStreamSerializationSupported< Bar >::value );
Такие штуки помогают при проверки соответствия переданного в шаблон типа различным концептам (в данном случае для соответствия концепту тип должен поддерживать ввод/вывод через потоки).
За сим я прощаюсь, всем спасибо за внимание! :)