January 10, 2020

BOOST :: MPL в естественной среде обитания.

Немного теории:

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

boost::mpl - это библиотека метапрограммирования,которая удовлетворяет потребность в наиболее востребованных и часто используемых шаблонах кодирования. Так же, как и STL дала нам концепцию итераторов, библиотека ускоренного метапрограммирования предоставляет итераторы типов и протокол классов мета-функций.

Метафункция - центральная абстракция MPL. Это шаблонный класс, все параметры являются типами. В открытом доступе находится вложенный элемент «::type/::value», в отличии от свойств может выполнять действия.

Имя_метафункции<аргументы>::type/::value;

Главное правило метафункций: все, что можно разделить и сделать более независимым должно быть сделано таковым. Такие решения способствуют более эффективному конструированию, обеспечивающее большую гибкость и свободу действий, чем одно монолитное решение, которое не поддается изменениям.


Стандартные примеры:

Одна из самых сильных сторон MPL, это концептуальное и синтаксическое сходство с STL. Алгоритмы и идиомы, которые программисты уже знают из STL, могут быть снова применены во время компиляции.

Большинство алгоритмов в mpl работают с последовательностями.
Например, поиск типа в списке выглядит следующим образом:

typedef mpl :: list <char, short, int, long, float, double> types;
typedef mpl :: find <types, long> :: type iter;

Здесь find принимает два параметра - последовательность для поиска ( types ) и значение ( long ) - и возвращает итератор iter, указывающий на первый элемент последовательности, так что iter :: type идентичен long .Если такого элемента не существует, iter идентичен end <types> :: type . По сути, именно так можно искать значение в std :: list или std :: vector , за исключением того, что mpl :: find принимает последовательность как один параметр, а std :: find занимает два итератора.


Есть возможность вызывать класс метафункций или лямбда-выражение с аргументами A1, ... An.

typedef mpl::apply< mpl::find<f>, list,long >::type iter;
typedef mpl::apply< lambda<f> , a1, ... an >::type lmd;

Даже если не использовать мета функцию lambda в MPL. Библиотека всё равно предоставляет средство использования лямбда-выражений во время компиляции.
Предположим, что мы хотим иметь лямбда-выражение, которое при вызове возвращает foo<Arg1,Arg2>. Такое лямбда-выражение может быть реализовано с помощью MPL Placeholder Expression - шаблон класса, созданный с одним или несколькими заполнителями:

typedef  foo < boost :: mpl :: _1 , boost :: mpl :: _2 >  foo_specifier ;

В данном случае foo_specifier это конкретный тип, такой же, как int или std::set<std::string>. Внутренние механизмы MPL способны обнаруживать, что этот тип был получен при создании экземпляра шаблона класса с заполнителями, boost::mpl::_1 и boost::mpl::_2, для первого и второго аргументов при вызове foo_specifier. Выраженные заполнители избавляют нас от необходимости писать типовые классы мета-функций.


Примеры из реальной жизни:

Пример ассоциативной последовательности, хранящей типы mpl::pair.

typedef boost::mpl::map<
	boost::mpl::pair<ITV8::GDRV::IVideoSource,		VideoSourceFactory>,
	boost::mpl::pair<ITV8::GDRV::IAudioSource,		AudioSourceFactory>,
	boost::mpl::pair<ITV8::GDRV::ITelemetry,		TelemetryFactory>,
	boost::mpl::pair<ITV8::GDRV::IIODevice,			IODeviceFactory>,
	boost::mpl::pair<ITV8::GDRV::IVideoAnalytics,	VideoAnaliticsFactory>,
	boost::mpl::pair<ITV8::GDRV::IAudioDestination,	AudioDestinationFactory>
> PanasonicDriverImplementationMap;

Map - может содержать не более одного элемента для каждого ключа.


Пример последовательности произвольного доступа, дополнительно поддерживающей операции back, push_back, pop_back.

typedef boost::mpl::vector<
	ITV8::Utility::MultipartFilter,
	ITV8::Utility::AudioBufferG7xxSendingFilter
> G7xxFilterChain;

Vector - самая простая и во многих случаях самая эффективная последовательность.


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

template<typename TFilterMap>
class MultiplexingFilter : public boost::mpl::inherit_linearly<TFilterMap,
	boost::mpl::inherit<boost::mpl::placeholders::_1,
		detail::FilterContainer<
			boost::mpl::first<boost::mpl::placeholders::_2>,
			boost::mpl::second<boost::mpl::placeholders::_2>
		>
	> >::type
{
    // ...
};

inherit_linearly по сути - это for each во время компиляции, который применяет типы из TFilterMap, к типу Node, который применяется к результату предыдущего "вызова". В данном случае Node - это boost::mpl::inherit, который строит иерархию из переданных ему типов.

mpl::inherit является прямой последовательностью.
Это концепция MPL, представляющая последовательность элементов во время компиляции.Метафункции mpl::first и mpl::second предоставляют итераторы, ограничивающие диапазон элементов последовательности.

Заполнитель в форме _n - это просто синоним соответствующей специализации arg <n>. Безымянный заполнитель. Имена заполнителей можно сделать доступными в пространстве имен пользователя с помощью mpl :: placeholder.

Таким образом у нас получается что-то вроде:

class MultiplexingFilter 
   : detail::FilterContainer<
            boost::mpl::first<TFilterMap::at<N - 1> >,
            boost::mpl::second<TFilterMap::at<N - 1> >
      > 
{
    // ...
};

Т.е. содержимое первого элемента TFilterMap наследуют в типе FilterContainer от пустого базового класса. Потом, что получилось, становится базовым классом для содержимого второго элемента нашего TFilterMap в типе FilterContainer. Так продолжается, пока элементы в TFilterMap не закончатся.

Это нужно, чтобы на этапе компиляции получить структуру вида:

class MultiplexingFilter 
{
     detail::FilterContainer<key_from_map1, value_from_map1> data1;
     detail::FilterContainer<key_from_map2, value_from_map2> data2;
     //.....
     detail::FilterContainer<key_from_mapN, value_from_mapN> dataN;
};

Лямбда выражения:

Лямбда выражения состоят из двух основных механизмов: классы метафункций и выражения с использованием заполнителей.

Можно сгенерировать класс метафункции из класса boost::add_pointer <_1>, используя лямбды MPL:

template <class X>
struct two_pointers
	: twice<typename mpl::lambda<boost::add_pointer<_1>>::type, X>	
{};

BOOST_STATIC_ASSERT
((
	boost::is_same
	<
		typename two_pointers<int>::type, 
		int**
	>::value
));

Т.е заполняемое выражение сохраняет результат между вызовами различных инстацнирований во внешней лямбда метафункции.

Хотя основная цель использования лямбды состоит в том, чтобы превратить placeholder выражения в классы метафункций, mpl::lambda может принять любое выражение лямбды, даже если это уже - класс метафункции. В этом случае лямбда возвращает свой неизменный аргумент. Алгоритмы MPL, такие как transform вызывают лямбду, прежде, чем обратиться к результату класса метафункции, это позволяет таким алгоритмам работать одинаково хорошо с любым видом лямбда выражений.

Применим эту стратегию к twice, чтобы не передавать в нее лямбду явно:

template <class F, class X>
struct twice : 
apply_helper
<
	typename mpl::lambda<F>::type, 
	typename apply_helper
	<
		typename mpl::lambda<F>::type, 
		X
	>::type
>
{};

Теперь можно использовать twice с классами метафункций и заполнителями:

int* x;

twice<add_pointer_f, int>::type          p = &x;		// передаем класс метафункции
twice<boost::add_pointer<_1>, int>::type q = &x;		// передаем метафункцию с заполнителем

Частичное приложение метафункции

Лямбды обеспечивают намного больше чем только способность передать метафункцию как аргумент, есть также дополнительные возможности, которые делают лямбды неотъемлемой частью почти каждой метапрограммной задачи.

Рассмотрите выражение лямбды mpl::plus<_1, _1>. Одинаковый аргумент передан обоим параметрам plus, таким образом число добавляется к самому себе. Двойная метафункция plus используется, чтобы создать унарное лямбда выражение. Другими словами, мы создали совершенно новое вычисление! Передавая не-заполнитель (non-placeholder) в качестве одного из аргументов, можно построить унарное лямбда выражение, которое добавляет постоянное значение, например, 42 к своему аргументу:

mpl::plus<_1, mpl::int_<42>>

Композиция метафункций:

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

mpl::multiplies<mpl::plus<_1,_2>, mpl::minus<_1,_2>>

Вычисляя лямбда выражение, MPL проверяет аргументы на предмет того, не являются ли они сами вложенными лямбда выражениями и если да, то они вычисляются в первую очередь, корректно передавая результат во внешнее выражение. Такая очередность работы с лямбда выражениями привносит большую гибкость и строгую логику.

MPL использует специальное правило, чтобы облегчить объединение обычных шаблонов в метапрограммы: после того, как все заполнители были заменены фактическими аргументами, если у получившейся специализации шаблона X нет вложенного «::type», результат лямбды является только X.

Например, mpl::apply<vector<_>, T> является всегда только vector<T>. Если бы не это поведение мы должны были бы создавать тривиальные метафункции для обеспечения обычных специализаций шаблона в лямбда выражениях:

// trivial std::vector generator
template<class U>
struct make_vector { typedef std::vector<U> type; };

typedef mpl::apply<make_vector<_>, T>::type vector_of_t;

Вместо этого достаточно написать лишь 1 строку:

typedef mpl::apply<std::vector<_>, T>::type vector_of_t;