суббота, 2 мая 2009 г.

Boost.Preprocessor

Препроцессор - штука достаточно неодназначная, что-бы это понять, достаточно поднять дискуссию на эту тему на каком-нибудь формуе, и опасная, так-как он вообще не в курсе синтаксиса языка программирования С++, ничего не знает о пространствах имен, шаблонах и тд. Лучше всего ограничить его применение условной компиляцией а так-же не забывать использовать #undef. Но, если не следовать этой рекомендации, то с его помощью можно делать очень интересные вещи :) Это можно продемонстрировать на примере списков типов, которые описаны в книге Александресску - "Современное проектирование на С++". Вот реализация такого списка
struct empty_t {};

template<class Head, class Tail>
struct cons
{
    typedef Head head;
    typedef Tail tail;
};
а использовать его можно так:
typedef cons< int, cons<long, cons< double, empty_t > > > tl_first;
Но это совсем не удобно, что-бы это исправить, нужен более простой способ объявления списков типов. Самый простой способ это сделать - макросы a-la Александресску:
#define TYPE_LIST_1(t) cons< t, empty_t >
#define TYPE_LIST_2(t1, t2) cons< t1, TYPE_LIST_1(t2) >
#define TYPE_LIST_3(t1, t2, t3) cons< t1, TYPE_LIST_2(t2, t3) >
typedef TYPE_LIST_3(int, long, double) tl_second;
Это просто и понятно, но не очень удобно, хотелось-бы избавиться от размера списка в имени макроса. То-же самое можно реализовать и без макросов, используя шаблоны:
template<class T1, class T2 = empty_t, class T3 = empty_t, class T4 = empty_t>
struct make_typelist;


template<class T1>
struct make_typelist<T1, empty_t, empty_t, empty_t>
{
    typedef cons<T1, empty_t> type;
};

template<class T1, class T2>
struct make_typelist<T1, T2, empty_t, empty_t>
{
    typedef cons<T1, cons<T2, empty_t> > type;
};

template<class T1, class T2, class T3>
struct make_typelist<T1, T2, T3, empty_t>
{
    typedef cons<T1, cons<T2, cons<T3, empty_t> > > type;
};
теперь достаточно написать:
typedef make_typelist<int, long, double>::type tl_third;
и у нас есть список типов. В этом коде мы сталкиваемся с одним из недостатков языка С++ - отсутствием шаблонов с переменным числом параметров, поэтому приходится описывать все необходимые специализации. Страшно представить, что будет, если нам потребуется создать список хотя-бы из десяти элементов. И вот здесь нам может помочь библиотека boost.preprocessor. С ее помощью можно написать вот такой нечитаемый код:
#include <boost/preprocessor/repetition.hpp>
//boost preprocessor
#define MAX_TYPELIST_SIZE 4

template< BOOST_PP_ENUM_PARAMS_WITH_A_DEFAULT(MAX_TYPELIST_SIZE, class T, empty_t)>
struct make_typelist;

#define TUPLE_PRINT(n, i, data) data
#define GEN_MAKETYPELIST(n, i, unused)                                  \
template< BOOST_PP_ENUM_PARAMS(i, class T) >                            \
struct make_typelist<                                                   \
      BOOST_PP_ENUM_PARAMS(i,T)                                         \
      BOOST_PP_COMMA_IF(i)                                              \
      BOOST_PP_ENUM(                                                    \
          BOOST_PP_SUB(MAX_TYPELIST_SIZE,i), TUPLE_PRINT, empty_t) >    \
{                                                                       \
    typedef BOOST_PP_ENUM_PARAMS(i, cons<T), empty_t                    \
            BOOST_PP_REPEAT(i, TUPLE_PRINT, > ) type;                   \
};

BOOST_PP_REPEAT_FROM_TO(1, MAX_TYPELIST_SIZE, GEN_MAKETYPELIST, ~)

typedef make_typelist<int, long, double>::type tl_third;

#undef TUPLE_PRINT
#undef GEN_MAKETYPELIST
В этом примере, препроцессор создаст точно такой-же код как и в предидущем примере. Но теперь, в случае если нам потребуется увеличить максимальный размер списка типов для make_typelist, достаточно изменить MAX_TYPELIST_SIZE. Многие библиотеки boost используют подобную технику для эмуляции шаблонов с переменным количеством параметров, например boost.tuple. В данном конкретном случае, макрос GEN_MAKETYPELIST создает одну из специализаций шаблонного класса make_typelist, в качестве параметров он получает максимальный размер списка типов(n), и количество параметров шаблона(i). Этот макрос используется в BOOST_PP_REPEAT_FROM_TO, для генерации всех необходимых специализаций шаблона. Сама библиотека boost.preprocessor - достаточно простая. Начать ее использовать очень легко, главное знать, для чего она может быть полезна.

4 комментария:

Evgeny Korostelev комментирует...

Списки типов это круто, но вот не понятно как сделать следуюшую вещь. Есть фабричный метод Create(вроде так называется это), который по int classuid создает объект. Так вот хочется иметь список типов, а не писать убер свич внутри. Вот только как это сделать. Перебор типов в рантайме-то не живет. Есть какие-нибудь идеи?
Еще меня интересовало как сделать по стандарту аналог мелкомягкого __COUNTER__. Хочется аналог enum автоматом проставлять. Ну знаете, бывают пустые структуры вместо enum, вот их бы означивать уникальными ID.
А зачем вообще это? Ну вот у меня сейчас живет фабрика, которая по ID создает конкретный объект, а регистрация доступных в SDK объектов производится путем использования структур и частичной специализации, что дает при незарегистрированном (не связанным на этапе компиляции) ID тип void. Соответственно в этих же структурах, кроме ID могут лежать какие-то еще описатели - так для удобства. И получается что-то вроде new OBJECT{id}::TYPE. Удобно так держать в одном месте таблицу типов и жить с ней.

Lazin комментирует...

Вместо uber switch можно использовать один прием, я его тут уже описывал:
http://evgeny-lazin.blogspot.com/2008/01/switch.html
его суть в том, что вместо того, что-бы просто хранить информацию о типах в compile-time, список типов может состоять из объектов, которые могут себя как нибудь вести. Если такой объект умеет хранить значение типа Head, то у нас получится tuple. Ну а если он умеет проверять некоторое условие и возвращать значение, либо рекурсивно продолжать проверку используя следующий в списке тип, то так уже можно эмулировать switch)

Evgeny Korostelev комментирует...
Этот комментарий был удален автором.
Lazin комментирует...

Вместо uber switch можно использовать один прием, я его тут уже описывал:
http://evgeny-lazin.blogspot.com/2008/01/switch.html
его суть в том, что вместо того, что-бы просто хранить информацию о типах в compile-time, список типов может состоять из объектов, которые могут себя как нибудь вести. Если такой объект умеет хранить значение типа Head, то у нас получится tuple. Ну а если он умеет проверять некоторое условие и возвращать значение, либо рекурсивно продолжать проверку используя следующий в списке тип, то так уже можно эмулировать switch)