21 January 2011

Шаблонная магия, метафункция IsValidExpression

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

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

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

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

За сим я прощаюсь, всем спасибо за внимание! :)
Tags:C++C++0xtemplatesметапрограммированиешаблонная магияненормальное программирование
Hubs: C++
+26
2k 38
Comments 14
Popular right now
C++ Developer
from 130,000 to 180,000 ₽QuadcodeСанкт-Петербург
Программист C++/Qt
from 100,000 to 180,000 ₽АМИКОНМоскваRemote job
C++ разработчик
from 80,000 ₽TRUSTSOFTКраснодар
Разработчик C++/Python
from 120,000 to 170,000 ₽L3 TechnologiesМосква
Senior C++/Python Developer
from 2,800 to 3,200 $Nitka Technologies, Inc.Remote job