template pattern matching
От: jazzer Россия Skype: enerjazzer
Дата: 02.03.10 04:34
Оценка: 384 (30) +2
Ну, может, не совсем 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()

Оставляю написание этого макроса вам в качестве развлечения

jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
boost mpl pattern matching overloading перегрузка сопоставление с образцом шаблоны
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.