Доброго времени суток, уважаемое Хабрасообщество.

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

Пример:

/* Определяем метафункцию 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 );

Такие штуки помогают при проверки соответствия переданного в шаблон типа различным концептам (в данном случае для соответствия концепту тип должен поддерживать ввод/вывод через потоки).

За сим я прощаюсь, всем спасибо за внимание! :)