Ну, может, не совсем pattern matching, ниже будет понятно, что я имею в виду.
Представьте, что Вы написали шаблонную функцию
template < class T >
std::string
f(T x)
{
return "generic";
}
Она замечательно работает, то Вам захотелось добавить специализацию для арифметических типов, т.е. чтобы для типов char, int, double вызывалась специальная функция
template < class T >
std::string
f(T x)
{
return "arithmetic";
}
а для всех остальных — самая общая функция.
Вопрос — как этого добиться?
В Boost есть замечательная штука — enable_if, которая возволяет "включить" определенную перегрузку функции, если удовлетворено некоторое условие.
В нашем случае это будет выглядеть так:
template < class T >
typename boost::enable_if< boost::is_arithmetic< T >, std::string >::type
f(T x)
{
return "arithmetic";
}
теперь эта специализация не будет рассматриваться для разрешения перегрузки, если тип аргумента не арифметический.
Однако если он арифметический, то сработают обе функции и мы получим неоднозначность в точке вызова.
Чтобы избавиться от неоднозначности, мы теперь должны "выключить" нашу самую обобщенную функцию при помощи аналогичного disable_if:
template < class T >
typename boost::disable_if< boost::is_arithmetic< T >, std::string >::type
f(T x)
{
return "generic";
}
Ура, теперь все работает!
До тех пор, пока мы не обнаружим, что у нас есть замечательный алгоритм для целых чисел, и нам срочно нужно его заюзать!
Мы радостно пишем
template < class T >
typename boost::enable_if< boost::is_integral< T >, std::string >::type
f(T x)
{
return "integral";
}
И... напарываемся на те же неоднозначности, но уже для целочисленных типов. Теперь нам нужно написать disable_if в арифметической версии, и целых 2 disable_if в обобщенной. А потом мы решаем добавить перегрузку для символьных типов и идем вешаться.
В чем же корень проблемы? В том, что матчинг у нас неупорядочен, в отличие от pattern matching в, скажем, Haskell — там все сверху вниз.
Стандартный ПМ через enable_if отлично работает в случае, когда условия не пересекаются, например:
if (i<100) ...;
/*else*/ if (i>100) ...;
/*else*/ if (i==100) ...;
тут мы можем эти ифы расставить в любом порядке, и нам не нужно писать else — результат будет точно такой же, что и без них.
Однако в этом случае:
if (i<0) ...;
else if (i<10) ...;
else if (i<100) ...;
else /*if (true)*/ ...;
мы уже не можем ни опустить else, ни переставить условия местами — результат изменится и очень сильно: во втором случае сработает не то условие, а в первом сработают сразу несколько, что в нашем случае выражается в неоднозначности. В случае перегрузки функций в С++ у нас нормального if-else нету, поэтому приходится переписывать эти условия на набор независимых условий:
if (i<0) ...;
if (i>=0 && i<10) ...;
if (i>=10 && i<100) ...;
if (i>=100) ...;
Внимание, вопрос — можно ли добиться, чтобы у нас было Haskell-gjдобное упорядочение перегрузок, без необходимости повторять одно и то же?
Т.е. если у нас есть некая последовательность условий
// char -> integral -> arithmetic -> all
template < class T >
struct Conditions
: boost::mpl::vector< boost::is_same< T, char >
, boost::is_integral< T >
, boost::is_arithmetic< T >
, boost::mpl::identity< boost::mpl::true_ >
>
{};
можно ли написать перегрузки так, чтоб они разрешались именно в этом порядке?
Оказывается, можно, и притом с помощью очень простого, похожего на enable_if, шаблонного заклинания:
// if T is char, then use this overload:
template < class T >
typename enable_cond_с< Conditions< T >, 0, const char* >::type
f(T x)
{
return "char";
}
// else if T is integral, then use this overload:
template < class T >
typename enable_cond_с< Conditions< T >, 1, std::string >::type
f(T x)
{
return "integral";
}
// else if T is arithmetic, then use this overload:
template < class T >
typename enable_cond_с< Conditions< T >, 2, std::string >::type
f(T x)
{
return "arithmetic";
}
// else use this overload:
template < class T >
typename enable_cond_с< Conditions< T >, 3, std::string >::type
f(T x)
{
return "generic";
}
Естественно, возвращаемый тип может быть разным, я для разнообразия сделал const char* для первой перегрузки.
Количество шаблонных аргументов также не ограничено одним, что дает возможность реализовывать разные веселые схемы перегрузки (у меня в проекте как раз была такая, поэтому я и начал писать нечто обобщенное).
enable_cond_с следует стандартной схеме именования, т.е. есть enable_cond, enable_cond_c, lazy_enable_cond и lazy_enable_cond_с.
Реализация ниже:
#include <boost/mpl/at.hpp>
#include <boost/mpl/size.hpp>
#include <boost/mpl/find_if.hpp>
#include <boost/mpl/iterator_range.hpp>
// Helper meta-function to take N first elements from Seq
template< class Seq, class N >
struct take
{
BOOST_MPL_ASSERT_MSG( boost::mpl::size< Seq >::type::value >= N::type::value
, SEQUENCE_IS_SHORTER_THAN
, (Seq, N) );
typedef typename boost::mpl::begin< Seq >::type first;
typedef typename boost::mpl::advance< first, N >::type last;
typedef typename boost::mpl::iterator_range< first, last > type;
};
// ordered matching metafunction
template< class CondSeq, class Index >
struct chain_cond_match
{
typedef typename take< CondSeq, Index >::type prev;
typedef typename
boost::mpl::and_< typename boost::mpl::at< CondSeq, Index >::type
, boost::is_same< typename boost::mpl::find_if< prev, boost::mpl::_ >::type
, typename boost::mpl::end< prev >::type
> >::type type;
};
// enable_cond family
template< class CondSeq, class Index, class Ret >
struct enable_cond
: boost::enable_if< typename chain_cond_match< CondSeq, Index >::type, Ret >
{};
template< class CondSeq, int i, class Ret >
struct enable_cond_c
: enable_cond< CondSeq, boost::mpl::int_< i >, Ret >
{};
template< class CondSeq, class Index, class Ret >
struct lazy_enable_cond
: boost::lazy_enable_if< typename chain_cond_match< CondSeq, Index >::type, Ret >
{};
template< class CondSeq, int i, class Ret >
struct lazy_enable_cond_c
: lazy_enable_cond< CondSeq, boost::mpl::int_< i >, Ret >
{};
Обратите внимание на BOOST_MPL_ASSERT_MSG — если кто не знал о существовании этого замечательного макроса: я всех призываю его использовать, он дает очень читабельные сообщения об ошибках, типа (правда, это после моего собственного постпроцессора ошибок):
test.h:32: error: ************::SEQUENCE_IS_SHORTER_THAN::************(Conditions<char>, mpl_::int_<7>)
Бонус
| Скрытый текст |
| С небольшой помощью макросов можно добиться такого синтаксиса, но он уже будет только для одного шаблонного аргумента:
OVERLOAD_START(F, 4)
OVERLOAD(F) const char*
CONDITION: boost::is_same< T, char >
FUNCTION(T x)
{
return "char";
}
OVERLOAD(F) std::string
CONDITION: boost::is_integral< T >
FUNCTION(T x)
{
return "integral";
}
OVERLOAD(F) std::string
CONDITION: boost::is_arithmetic< T >
FUNCTION(T x)
{
return "arithmetic";
}
OVERLOAD(F) std::string
CONDITION: boost::mpl::true_
FUNCTION(T x)
{
return "generic";
}
OVERLOAD_END()
Оставляю написание этого макроса вам в качестве развлечения |
| |