11 нояб. 2009 г.

Ликбез по Boost.Preprocessor

Часто приходится слышать вздыхания коллег и вопросы "Почему так и зачем?" при виде например такого:
#define BOOST_FUSION_ADAPT_STRUCT(name, bseq)                       \
    BOOST_FUSION_ADAPT_STRUCT_I(                                    \
        name, BOOST_PP_CAT(BOOST_FUSION_ADAPT_STRUCT_X bseq, 0))    \
    /***/

#define BOOST_FUSION_ADAPT_STRUCT_X(x, y)    \
    ((x, y)) BOOST_FUSION_ADAPT_STRUCT_Y
#define BOOST_FUSION_ADAPT_STRUCT_Y(x, y)    \
    ((x, y)) BOOST_FUSION_ADAPT_STRUCT_X
#define BOOST_FUSION_ADAPT_STRUCT_X0
#define BOOST_FUSION_ADAPT_STRUCT_Y0
Я же начинаю грустить с мыслями "ипать-колотить, ну тут же всё просто!"...

Поэтому, дабы разбавить свой залежавшийся блог свежачком, я решил написать более-менее подробный ликбез о простом, а именно о Boost.Preprocessor.
Приведённый пример (boost/fusion/adapted/struct/adapt_struct.hpp) я разберу в конце статьи.

Заранее прошу простить меня за отсутствие писательского таланта, возможные ошибки и некоторую сухость изложения. В защиту сухого изложения могу сказать, что такую статью проще читать и использовать по частям, нежели чем следить за лихой и весёлой мыслей автора. Замечания и корректировки приветствуются. (:

Boost.Preprocessor (далее BP) - это библиотека, делающая метапрограммирование с помощью препроцессора простым, более эффективным и, при правильном пользовании и должном умении, читабельным (тут, конечно же, без ответного мало-мальского умения от читателя такого кода тоже не обойтись).

Начну с того что в BP определены такие типы данных как:
  • Список (list)
    Состоит из пар, первый элемент которых - объект1, второй - либо список, либо конец списка, т.е. BOOST_PP_NIL.
    Имеет запись: «(e1, (e2, ...(en, BOOST_PP_NIL)...))»,
    где e1..n - элементы списка, а BOOST_PP_NIL - макрос обозначающий конец списка.
    Например: «(1, (2, (3, BOOST_PP_NIL)))» - список с 3-мя числами 1, 2 и 3.
  • Кортеж (tuple)
    Состоит из элементов разделённых запятыми.
    Имеет запись: «(e1, e2, ..., en)»,
    где e1..n - элементы последовательности.
    Например: «(alpha, beta, gamma)» - кортеж с 3-мя элементами alpha, beta и gamma.
  • Массив (array)
    Кортеж из двух элементов, первый из которых - размер массива, второй - кортеж, содержащий элементы массива.
    Удобнее кортежей тем, что размер массива уже известен BP, в часности отпадает необходимость явно указывать размер.
    Имеет запись: «(n, (e1, e2, ..., en))»,
    где n - размер массива, e1..n - его элементы.
    Например: «(3, (alpha, beta, gamma))» - массив с 3-мя элементами alpha, beta и gamma.
  • Последовательность (sequence)
    Список элементов заключённых в скобки и, необязательно, разделённых пробелами.
    Очень полезны когда нужно передать макросу переменное количество объектов1.
    Имеет запись: «(e1) (e2) ... (en)»,
    где e1..n - элементы последовательности.
    Например: «(alpha) (beta) (gamma)» - последовательность с элементами alpha, beta и gamma.

Для выше перечисленных типов определены такие операции и алгоритмы как Extract, Insert, PopFront, Iterate, ForEach, Transform, Fold, Enum и т.д и т.п.2 Полный список и описание всех макросов может быть найдено в документации в разделе "Reference".
Опишу только некоторые из них (в большинстве своём те которые мне пригодились на своей нелёгкой службе):
  • BOOST_PP_TUPLE_ELEM(size, i, tuple)
    Извлекает i-тый элемент из кортежа tuple, размер которого size.
    Пример (в комментариях результат раскрытия макроса):
    BOOST_PP_TUPLE_ELEM(3, 0, (alpha, beta, gamma))
    // alpha
    BOOST_PP_TUPLE_ELEM(3, 2, (alpha, beta, gamma))
    // gamma
  • BOOST_PP_ARRAY_ELEM(i, array)
    Извлекает i-тый элемент из массива array.
    Пример (в комментариях результат раскрытия макроса):
    BOOST_PP_ARRAY_ELEM(0, (alpha, beta, gamma))
    // alpha
    BOOST_PP_ARRAY_ELEM(2, (alpha, beta, gamma))
    // gamma
  • BOOST_PP_CAT(a, b)
    Реализует конкатенацию. В отличие от оператора ##, сначала раскрывает аргументы a и b.
    Пример:
    BOOST_PP_CAT(x, BOOST_PP_CAT(y, z))
    // xyz
    
  • BOOST_PP_ENUM(count, macro, data)
  • BOOST_PP_ENUM_SHIFTED(count, macro, data)
  • BOOST_PP_ENUM_TRAILING(count, macro, data)
    Реализует алгоритм Enum для генерирации списка из элементов macro(z, i, data), разделённых запятыми, где z - ненужный нам параметр (далее в статье просто игнорируется, а на его место подставляется тильда), i - номер итерации, data - значение переданное через BOOST_PP_ENUM.
    Пример:
    #define MY_MACRO(z, i, text) text ## i
    
    MY_MACRO(~, 42, SomeType)
    // SomeType42
    
    BOOST_PP_ENUM(3, MY_MACRO, SomeType)
    // SomeType0, SomeType1, SomeType2
    
    BOOST_PP_ENUM_SHIFTED(3, MY_MACRO, SomeType)
    // SomeType1, SomeType2
    
    BOOST_PP_ENUM_TRAILING(3, MY_MACRO, SomeType)
    // , SomeType0, SomeType1, SomeType2
    
  • BOOST_PP_ENUM_PARAMS(count, param)
  • BOOST_PP_ENUM_BINARY_PARAMS(count, p1, p2)
  • BOOST_PP_ENUM_PARAMS_WITH_A_DEFAULT(count, param, def)
    Реализуют алгоритм Enum в различных вариациях. Приведу только примеры.
    Пример:
    void fn(BOOST_PP_ENUM_PARAMS(3, int par)) {...}
    // void fn(int par0, int par1, int par2) {...}
    
    void fn(BOOST_PP_ENUM_BINARY_PARAMS(3, Type, par)) {...}
    // void fn(Type0 par0, Type1 par1, Type2 par2) {...}
    
    void fn(BOOST_PP_ENUM_PARAMS_WITH_A_DEFAULT(3, int par, 42)) {...}
    // void fn(int par0 = 42, int par1 = 42, int par2 = 42) {...}
    
  • BOOST_PP_SEQ_TRANSFORM(op, data, seq)
    Реализует алгоритм Transform для последовательности seq. В результате работы раскрывается в «(op(~, data, e1)) (op(~, data, e2)) ... (op(~, data, en))»,
    где e1..n - элемент последовательности.
    Пример:
    #define ADD_POSTFIX(z, data, el) el ## data
    
    ADD_POSTFIX(~, Postfix, Type)
    // TypeSomePostfix
    
    BOOST_PP_SEQ_TRANSFORM(ADD_POSTFIX, T, (Alpha) (Beta) (Gamma))
    // (AlphaT) (BetaT) (GammaT)
    
  • BOOST_PP_SEQ_FOR_EACH(macro, data, seq)
    То же самое что и BOOST_PP_SEQ_TRANSFORM, только на выходе не создаётся последовательность.
    Пример:
    BOOST_PP_SEQ_FOR_EACH(ADD_POSTFIX, T, (Alpha) (Beta) (Gamma))
    // AlphaT BetaT GammaT
    
  • BOOST_PP_SEQ_ENUM(seq)
    Реализует алгоритм Enum для последовательности seq.
    Пример:
    BOOST_PP_SEQ_ENUM((Alpha) (Beta) (Gamma))
    // Alpha, Beta, Gamma
    
    BOOST_PP_SEQ_ENUM(
      BOOST_PP_SEQ_TRANSFORM(
        ADD_POSTFIX, T, (Alpha) (Beta) (Gamma)))
    // AlphaT, BetaT, GammaT
    
В BP определенно ещё куча маскросов о которых можно прочитать в документации.

Теперь приведу маленький пример, где BP был бы полезен. Допустим необходимо задавать структуры с полями различных типов и определённым порядком. При этом в каждой из них хочется иметь некий метод apply, позволяющий применить функтор ко всем её полям в том же порядке, что и при их определении.
Для этого можно определить макрос DEFINE_OUR_STRUCT принимающий последовательность из кортежей вида (тип-поля, имя-поля). Чтобы объявить поля структуры и перечислить применения функтора в apply будем использовать алгоритм ForEach.

Получается такой код:
#include <boost/preprocessor.hpp>

#define DEFINE_OUR_STRUCT(name, seq) DEFINE_OUR_STRUCT_I(name, seq)

#define DEFINE_OUR_STRUCT_I(name, seq)                   \
  struct name {                                          \
    DEFINE_OUR_STRUCT_ENUM_FIELDS(seq)                   \
                                                         \
    template <typename functor>                          \
    void apply(Functor functor) {                        \
      DEFINE_OUR_STRUCT_ENUM_APPLY_FIELDS(functor, seq)  \
    }                                                    \
  };

#define DEFINE_OUR_STRUCT_EXTRACT_TYPE(tuple)   \
  BOOST_PP_TUPLE_ELEM(2, 0, tuple)

#define DEFINE_OUR_STRUCT_EXTRACT_NAME(tuple)   \
  BOOST_PP_TUPLE_ELEM(2, 1, tuple)

#define DEFINE_OUR_STRUCT_ENUM_FIELDS(seq)              \
  BOOST_PP_SEQ_FOR_EACH(                                \
    DEFINE_OUR_STRUCT_ENUM_FIELDS_OP, ~, seq)

#define DEFINE_OUR_STRUCT_ENUM_FIELDS_OP(z, data, el)   \
  DEFINE_OUR_STRUCT_EXTRACT_TYPE(el)                    \
  DEFINE_OUR_STRUCT_EXTRACT_NAME(el);

#define DEFINE_OUR_STRUCT_ENUM_APPLY_FIELDS(ft, seq)    \
  BOOST_PP_SEQ_FOR_EACH(                                \
    DEFINE_OUR_STRUCT_ENUM_APPLY_FIELDS_OP, ft, seq)

#define DEFINE_OUR_STRUCT_ENUM_APPLY_FIELDS_OP(z, ft, el) \
  ft(DEFINE_OUR_STRUCT_EXTRACT_NAME(el));

// Всё готово чтобы объявить нашу первую структуру.
DEFINE_OUR_STRUCT(first_struct,
                  ((int               , id))
                  ((std::vector<char> , data))
  )
// Что в итоге раскроется в:
struct first_struct {
  int                   id;
  std::vector<char>     data;

  template <typename Functor>
  void apply(Functor functor) {
    functor(id);
    functor(data);
  }
};

Теперь вернёмся к примеру который я дал в начале:
#define BOOST_FUSION_ADAPT_STRUCT(name, bseq)                       \
    BOOST_FUSION_ADAPT_STRUCT_I(                                    \
        name, BOOST_PP_CAT(BOOST_FUSION_ADAPT_STRUCT_X bseq, 0))    \
    /***/

#define BOOST_FUSION_ADAPT_STRUCT_X(x, y)    \
    ((x, y)) BOOST_FUSION_ADAPT_STRUCT_Y
#define BOOST_FUSION_ADAPT_STRUCT_Y(x, y)    \
    ((x, y)) BOOST_FUSION_ADAPT_STRUCT_X
#define BOOST_FUSION_ADAPT_STRUCT_X0
#define BOOST_FUSION_ADAPT_STRUCT_Y0

Здесь я хочу показать для чего эта мистика с X0 и Y0. Из документации по Boost.Fusion видно как используется BOOST_FUSION_ADAPT_STRUCT:
BOOST_FUSION_ADAPT_STRUCT(
    struct_name,
    (member_type0, member_name0)
    (member_type1, member_name1)
    ...
    )

Сейчас неважно какова цель макроса BOOST_FUSION_ADAPT_STRUCT, но нужно заметить, что в него передаётся последовательность. И тут возникает маленькая проблемка: «(member_type0, member_name0) (member_type1, member_name1)» - элементы последовательности не кортежи из которых можно было бы извлечь их элементы, а простой код (для препроцессора - текст) внутри которого содержится запятая. Поэтому каждый элемент оборачивают в скобки и, вуаля, они становятся кортежами:
BOOST_PP_CAT(BOOST_FUSION_ADAPT_STRUCT_X (a, 1) (b, 2) (c, 3), 0)
// ((a, 1)) ((b, 2)) ((c, 3))

Как это работает:
  1. BOOST_PP_CAT(
        BOOST_FUSION_ADAPT_STRUCT_X (a, 1) (b, 2) (c, 3),
        0)
  2. Вычислим первый аргумент BOOST_PP_CAT:
    1. BOOST_FUSION_ADAPT_STRUCT_X (a, 1) (b, 2) (c, 3)
    2. ((a, 1)) BOOST_FUSION_ADAPT_STRUCT_Y (b, 2) (c, 3)
    3. ((a, 1)) ((b, 2)) BOOST_FUSION_ADAPT_STRUCT_X (c, 3)
    4. ((a, 1)) ((b, 2)) ((c, 3)) BOOST_FUSION_ADAPT_STRUCT_Y
  3. BOOST_PP_CAT(
        ((a, 1)) ((b, 2)) ((c, 3)) BOOST_FUSION_ADAPT_STRUCT_Y,
        0)
  4. ((a, 1)) ((b, 2)) ((c, 3)) BOOST_FUSION_ADAPT_STRUCT_Y0
  5. ((a, 1)) ((b, 2)) ((c, 3))
Всё оказалось довольно-таки просто, ага? Такой же приём можно для удобства применить и к нашему DEFINE_OUR_STRUCT.

В пользу макросов хочется сказать, что их просто отлаживать: «g++ -E file.cxx». А если используется Emacs, то достаточно выделить и «M-x c-macro-expand» или «C-c C-e».

P.S. oooops, если захочем DEFINE_OUR_STRUCT(first_struct, ((char, date[10]))).

UPDATE:
PP.S. boost::array<> нам в руки.


1. Под объектом понимается какой-либо экземпляр типа или код в текстовом представлении.
2. Про операции и алгоритмы я выдумал сам только для того чтобы кратко показать каких классов определены макросы (надеюсь это удалось) и должны восприниматься только на интуитивном уровне, не пытайтесь это искать в документации, разве что только среди имён макросов.

7 комментариев:

  1. Здравствуйте.
    Скажите, почему в некоторых примерах декларации кортежей, Вы используете одну пару скобок, а в конечном примере со структурой, Вы используете две пары скобок для записи членов структуры?

    Еще вопрос: Каким образом можно определить кол-во элементов в кортеже?

    Спасибо.

    ОтветитьУдалить
  2. Привет.

    Мы можем использовать одинарную пару скобок, но это уже будет не последовательность (sequence), а просто записанные подряд кортежи (tuples). Чтобы иметь возможность с ними работать нам необходимо привести их к последовательности, проще говоря, обернуть каждый кортеж в скобки. Так они станут элементами последовательности с которой уже можно работать.
    Использовал я двойную пару скобок для упрощения и просто потому что приём оборачивания кортежей в дополнительные скобки объяснён ниже.

    Узнать размер кортежа в рамках стандартного C++ нельзя, так как в нём нет variadic macros, но их можно найти в расширениях компилятора, например, GCC. По этой ссылке можно увидеть как вычисляется количество аргументов макроса, а они ничем не отличаются от кортежа.

    ОтветитьУдалить
  3. Здравствуйте.
    Позвольте еще несколько вопросов.
    1. каким образом можно заставить препроцессор выполнить переход на новую строку?
    2. каким образом можно заставить препроцессор вставить символ табуляции или несколько пробелов?
    3. каким образом можно заставить препроцессор вставить в препроцессируемый код комментарии?

    Спасибо.

    ОтветитьУдалить
  4. Не знаю, никогда не задавался этими вопросами. Более того, не вижу смысла пытаться это делать.
    Если препроцессором пытаться генерировать комментарии, то непонятно кто их будет читать, да и компилятор Вас не совсем поймёт, так как комментарии вырезаются до препроцессора.
    Если Вы используете препроцессор отдельно, то для таких задач можно найти более адекватные средства.
    Что Вы там такое пишете, если не секрет?

    ОтветитьУдалить
  5. пишу метагенератор. точнее уже написал, но подумал добавить комменты в генерируемый код, на будущее, для отладки)

    ОтветитьУдалить