![]() |
![]() ![]() ![]() ![]() ![]() |
![]() |
Users' GuideBoost , The Boost C++ Libraries BoostBook Documentation Subset , Chapter 29. Boost.Proto
|
![]() | Note |
---|---|
Пожалуйста, отправьте любые вопросы, комментарии и отчеты об ошибках в eric |
Proto - это большая библиотека, которая, вероятно, не похожа ни на одну библиотеку, которую вы использовали раньше. Proto использует некоторые согласованные соглашения об именах, чтобы упростить навигацию, и они описаны ниже.
Все функции Прото определены в пространстве имен<boost::proto
>. Например, существует функция под названием<value()
>, определенная в<boost::proto
>, которая принимает терминальное выражение и возвращает значение терминала.
Прото определяетметафункции, которые соответствуют каждой из свободных функций Прото. Метафункции используются для вычисления типов возврата функций. Все метафункции Прото живут в пространстве имен<boost::proto::result_of
>и имеют то же название, что и функции, которым они соответствуют. Например, существует шаблон класса<boost::proto::result_of::value<>
>, который можно использовать для вычисления типа возврата функции<boost::proto::value()
>.
Прото определяетобъект функцииэквиваленты всех его свободных функций. (Объект функции — это экземпляр типа класса, который определяет функцию члена<operator()
>.) Все типы объектов функции Прото определены в пространстве имен<boost::proto::functional
>и имеют то же название, что и их соответствующие свободные функции. Например,<boost::proto::functional::value
>— класс, который определяет объект функции, который делает то же самое, что<boost::proto::value()
>свободная функция.
Прото также определяетпримитивные преобразования— типы классов, которые могут использоваться для составления более крупных преобразований для манипулирования деревьями экспрессии. Многие свободные функции Прото имеют соответствующие примитивные преобразования. Они живут в пространстве имен<boost::proto
>, и их имена имеют важное значение. Например, преобразование, соответствующее функции<value()
>, называется<boost::proto::_value
>.
В нижеследующей таблице резюмируется вышеупомянутое обсуждение:
Table 29.1. Proto Naming Conventions
сущность |
Пример |
---|---|
Свободная функция | < |
Метафункция | < |
Функциональный объект | < |
Трансформация | < |
Ниже приведена очень простая программа, которая использует Proto для создания шаблона выражения, а затем выполняет его.
#include <iostream> #include <boost/proto/proto.hpp> #include <boost/typeof/std/ostream.hpp> using namespace boost; proto::terminal< std::ostream & >::type cout_ = { std::cout }; template< typename Expr > void evaluate( Expr const & expr ) { proto::default_context ctx; proto::eval(expr, ctx); } int main() { evaluate( cout_ << "hello" << ',' << " world" ); return 0; }
Эта программа предусматривает следующее:
hello, world
Эта программа создает объект, представляющий операцию вывода, и передает его функции<evaluate()
>, которая затем выполняет его.
Основная идея шаблонов выражения состоит в том, чтобы перегрузить всех операторов, чтобы вместо немедленной оценки выражения они построили древовидное представление выражения, чтобы его можно было оценить позже. Для каждого оператора в выражении, по крайней мере, один операнд должен быть Protofied для того, чтобы оператор Proto был найден. В выражении...
cout_ << "hello" << ',' << " world"
Протофидное подвыражение<cout_
>, которое является протоификацией<std::cout
>. Наличие<cout_
>«заражает» выражение и учитывает перегрузки оператора строительства деревьев Прото. Любые буквы в выражении затем Protofied, обернув их в терминал Proto, прежде чем они будут объединены в более крупные выражения Proto.
После того, как операторские перегрузки Proto построили дерево экспрессии, выражение может быть лениво оценено позже. Это то, что<proto::eval()
>. Это общий оценщик выражений ходьбы по дереву, поведение которого настраивается черезконтекстпараметр. Использование<proto::default_context
>присваивает операторам стандартные значения в выражении. (Используя другой контекст, вы можете дать операторам в своих выражениях различную семантику. По умолчанию Proto не делает никаких предположений о том, что на самом деле означают операторы.
Прежде чем продолжить, давайте используем приведенный выше пример, чтобы проиллюстрировать важный принцип проектирования Proto. Шаблон выражения, созданный в примереHello World, является полностью общим и абстрактным. Он никоим образом не привязан к какой-либо конкретной области или приложению и не имеет какого-либо конкретного значения или поведения самостоятельно, пока не будет оценен вконтексте. Шаблоны выражения на самом деле просто неоднородные деревья, что может означать что-то в одной области, а что-то совершенно другое в другой.
Как мы увидим позже, существует способ создания деревьев выражения Прото, которые являются, а нечисто абстрактными, и которые имеют смысл и поведение, независимое от любого контекста. Существует также способ контролировать, какие операторы перегружены для вашего конкретного домена. Но это не поведение по умолчанию. Позже мы увидим, почему дефолт часто бывает хорошим.
"Здравствуй, мир" - это хорошо, но это не очень далеко. Давайте использовать Proto для создания EDSL (встроенного доменного языка) для калькулятора с ленивой оценкой. Мы посмотрим, как определить терминалы в мини-языке, как составить их в более крупные выражения и как определить контекст оценки, чтобы ваши выражения могли выполнять полезную работу. Когда мы закончим, у нас будет мини-язык, который позволит нам объявить лениво оцененное арифметическое выражение, такое как<(_2
-_1)/_2
*100
>, где<_1
>и<_2
>являются держателями для значений, которые будут переданы при оценке выражения.
Первый из них – это<_1
>и<_2
>. Для этого мы используем метафункцию<proto::terminal<>
>.
// Define a placeholder type template<int I> struct placeholder {}; // Define the Protofied placeholder terminals proto::terminal<placeholder<0> >::type const _1 = {{}}; proto::terminal<placeholder<1> >::type const _2 = {{}};
Поначалу инициализация может показаться немного странной, но есть веская причина для этого. Объекты<_1
>и<_2
>выше не требуют построения времени выполнения — онистатически инициализированы, что означает, что они по существу инициализированы во время компиляции. См. разделСтатическая инициализацияв приложенииОбоснованиедля получения дополнительной информации.
Теперь, когда у нас есть терминалы, мы можем использовать перегрузки оператора Proto, чтобы объединить эти терминалы в более крупные выражения. Например, мы можем сразу сказать такие вещи, как:
// This builds an expression template (_2 - _1) / _2 * 100;
Это создает дерево экспрессии с узлом для каждого оператора. Тип полученного объекта большой и сложный, но мы не очень заинтересованы в нем сейчас.
До сих пор объект — это просто дерево, представляющее выражение. У него нет поведения. В частности, это еще не калькулятор. Ниже мы рассмотрим, как сделать его калькулятором, определив контекст оценки.
Не сомневайтесь, вы хотите, чтобы ваши шаблоны выражения на самом деледелаличто-то. Один из подходов заключается в определенииконтекста оценки. Контекст подобен объекту функции, который связывает поведение с типами узлов в дереве экспрессии. Следующий пример должен прояснить это. Это объясняется ниже.
struct calculator_context : proto::callable_context< calculator_context const > { // Values to replace the placeholders std::vector<double> args; // Define the result type of the calculator. // (This makes the calculator_context "callable".) typedef double result_type; // Handle the placeholders: template<int I> double operator()(proto::tag::terminal, placeholder<I>) const { return this->args[I]; } };
В<calculator_context
>мы укажем, как Proto должен оценивать терминалы заполнителя, определяя соответствующие перегрузки оператора вызова функции. Для любых других узлов в дереве экспрессии (например, арифметических операций или неплацентарных терминалов) Proto будет оценивать экспрессию «по умолчанию». Например, двоичный узел плюс оценивается путем первой оценки левого и правого операндов и добавления результатов. Оценка по умолчанию Proto использует библиотекуBoost.Typeofдля вычисления типов возврата.
Теперь, когда у нас есть контекст оценки для нашего калькулятора, мы можем использовать его для оценки наших арифметических выражений.
calculator_context ctx; ctx.args.push_back(45); // the value of _1 is 45 ctx.args.push_back(50); // the value of _2 is 50 // Create an arithmetic expression and immediately evaluate it double d = proto::eval( (_2 - _1) / _2 * 100, ctx ); // This prints "10" std::cout << d << std::endl;
Позже мы увидим, как определить более интересные контексты оценки и преобразования выражения, которые дают вам полный контроль над тем, как оцениваются ваши выражения.
Наш калькулятор EDSL уже довольно полезен, и для многих сценариев EDSL больше не потребуется. Но давайте продолжим. Представьте, насколько было бы лучше, если бы все выражения калькулятора перегрузились<operator()
>, чтобы их можно было использовать в качестве функциональных объектов. Мы можем сделать это, создав калькулятордомени сообщив Proto, что все выражения в домене калькулятора имеют дополнительные члены. Вот как определить домен калькулятора:
// Forward-declare an expression wrapper template<typename Expr> struct calculator; // Define a calculator domain. Expression within // the calculator domain will be wrapped in the // calculator<> expression wrapper. struct calculator_domain : proto::domain< proto::generator<calculator> > {};
Тип<calculator<>
>будет экспрессионной оберткой. Он будет вести себя так же, как выражение, которое он обертывает, но у него будут дополнительные функции, которые мы определим.<calculator_domain
>- это то, что сообщает Прото о нашей обертке. Приведены ниже в определении<calculator<>
>. Читайте дальше для описания.
// Define a calculator expression wrapper. It behaves just like // the expression it wraps, but with an extra operator() member // function that evaluates the expression. template<typename Expr> struct calculator : proto::extends<Expr, calculator<Expr>, calculator_domain> { typedef proto::extends<Expr, calculator<Expr>, calculator_domain> base_type; calculator(Expr const &expr = Expr()) : base_type(expr) {} typedef double result_type; // Overload operator() to invoke proto::eval() with // our calculator_context. double operator()(double a1 = 0, double a2 = 0) const { calculator_context ctx; ctx.args.push_back(a1); ctx.args.push_back(a2); return proto::eval(*this, ctx); } };
Структура<calculator<>
>является выражениемрасширения. Он использует<proto::extends<>
>для эффективного добавления дополнительных членов к типу выражения. При составлении более крупных выражений из более мелких Proto отмечает, в какой области находятся меньшие выражения. Большее выражение находится в том же домене и автоматически обернуто в обертку расширения домена.
Все, что еще предстоит сделать, это поместить наших держателей в область калькулятора. Мы делаем это, завернув их в нашу обертку<calculator<>
>, как показано ниже:
// Define the Protofied placeholder terminals, in the // calculator domain. calculator<proto::terminal<placeholder<0> >::type> const _1; calculator<proto::terminal<placeholder<1> >::type> const _2;
Любое более крупное выражение, содержащее эти заполнители, будет автоматически обернуто в обертку<calculator<>
>и иметь нашу<operator()
>перегрузку. Это означает, что мы можем использовать их в качестве функциональных объектов следующим образом.
double result = ((_2 - _1) / _2 * 100)(45.0, 50.0); assert(result == (50.0 - 45.0) / 50.0 * 100));
Поскольку выражения калькулятора теперь являются действительными функциональными объектами, мы можем использовать их со стандартными алгоритмами, как показано ниже:
double a1[4] = { 56, 84, 37, 69 }; double a2[4] = { 65, 120, 60, 70 }; double a3[4] = { 0 }; // Use std::transform() and a calculator expression // to calculate percentages given two input sequences: std::transform(a1, a1+4, a2, a3, (_2 - _1) / _2 * 100);
Теперь давайте воспользуемся примером калькулятора, чтобы изучить некоторые другие полезные функции Proto.
Возможно, вы заметили, что вам не нужно определять перегруженный<operator-()
>или<operator/()
>. Фактически, Proto перегружаетвсехоператоров для вас, даже если они могут ничего не значить на вашем доменном языке. Это означает, что может быть возможно создать выражения, которые являются недействительными в вашем домене. Вы можете обнаружить недействительные выражения с помощью Proto, определивграмматикувашего доменного языка.
Для простоты предположим, что наш калькулятор EDSL должен разрешать только сложение, вычитание, умножение и деление. Любое выражение, связанное с любым другим оператором, является недействительным. Используя Proto, мы можем определить это требование, определив грамматику калькулятора EDSL. Это выглядит следующим образом:
// Define the grammar of calculator expressions struct calculator_grammar : proto::or_< proto::plus< calculator_grammar, calculator_grammar > , proto::minus< calculator_grammar, calculator_grammar > , proto::multiplies< calculator_grammar, calculator_grammar > , proto::divides< calculator_grammar, calculator_grammar > , proto::terminal< proto::_ > > {};
Вы можете прочитать приведенную выше грамматику следующим образом: дерево выражений соответствует грамматике калькулятора, если это двоичный плюс, минус, умножает или делит узел, где оба детских узла также соответствуют грамматике калькулятора; или если это терминал. В грамматике Прото<proto::_
>- это карта, которая соответствует любому типу, поэтому<proto::terminal<
proto::_>
>соответствует любому терминалу, будь то заполнитель или буквальный.
![]() | Note |
---|---|
Эта грамматика на самом деле немного свободнее, чем нам бы хотелось. Только заполнители и буквалы, которые конвертируются в дублеры, являются действительными терминалами. Позже мы увидим, как выразить подобные вещи в протограмматике. |
После того, как вы определили грамматику EDSL, вы можете использовать метафункцию<proto::matches<>
>, чтобы проверить, соответствует ли данный тип выражения грамматике. Например, мы можем добавить следующее к нашей перегрузке<calculator::operator()
>:
template<typename Expr> struct calculator : proto::extends< /* ... as before ... */ > { /* ... */ double operator()(double a1 = 0, double a2 = 0) const { // Check here that the expression we are about to // evaluate actually conforms to the calculator grammar. BOOST_MPL_ASSERT((proto::matches<Expr, calculator_grammar>)); /* ... */ } };
Добавление строки<BOOST_MPL_ASSERT()
>приводит к тому, что мы оцениваем только те выражения, которые соответствуют грамматике калькулятора EDSL. С грамматикой Proto<proto::matches<>
>и<BOOST_MPL_ASSERT()
>очень легко дать пользователям ваши короткие и читаемые ошибки времени компиляции EDSL, когда они случайно неправильно используют ваш EDSL.
![]() | Note |
---|---|
< |
Грамматики и<proto::matches<>
>позволяют обнаружить, когда пользователь создал недействительное выражение, и выпустить ошибку времени компиляции. Но что, если вы хотите помешать пользователям создавать недействительные выражения? Используя грамматику и домены вместе, вы можете отключить любую из перегрузок оператора Proto, которые создадут недействительное выражение. Это так же просто, как указать грамматику EDSL при определении домена, как показано ниже:
// Define a calculator domain. Expression within // the calculator domain will be wrapped in the // calculator<> expression wrapper. // NEW: Any operator overloads that would create an // expression that does not conform to the // calculator grammar is automatically disabled. struct calculator_domain : proto::domain< proto::generator<calculator>, calculator_grammar > {};
Единственное, что мы изменили, это добавили<calculator_grammar
>в качестве второго параметра шаблона к<proto::domain<>
>шаблону при определении<calculator_domain
>. С помощью этого простого дополнения мы отключаем любые перегрузки оператора Proto, которые создадут недействительное выражение калькулятора.
Надеюсь, это даст вам представление о том, что Proto может сделать для вас. Но это только царапает поверхность. Остальная часть этого руководства пользователя будет более подробно описывать все эти функции и другие.
Счастливого метапрограммирования!
Вот забавная часть: разработка собственного языка мини-программирования. В этом разделе мы поговорим о гайках и болтах проектирования интерфейса EDSL с использованием Proto. Мы рассмотрим определение терминалов и ленивых функций, с которыми пользователи вашего EDSL будут программировать. Мы также поговорим о перегрузках оператора построения шаблонов Proto и о способах добавления дополнительных участников к выражениям в вашем домене.
Как мы видели в примере с калькулятором из Введения, самый простой способ получить EDSL и запустить его - это просто определить некоторые терминалы следующим образом.
// Define a literal integer Proto expression. proto::terminal<int>::type i = {0}; // This creates an expression template. i + 1;
С некоторыми терминалами и перегрузками оператора Proto вы можете сразу же начать создавать шаблоны выражения.
Определение терминалов — с агрегированной инициализацией — иногда может быть немного неудобным. Proto обеспечивает более простую в использовании обертку для букв, которая может быть использована для построения терминальных выражений Protofied. Он называется<proto::literal<>
>.
// Define a literal integer Proto expression. proto::literal<int> i = 0; // Proto literals are really just Proto terminal expressions. // For example, this builds a Proto expression template: i + 1;
Также существует функция<proto::lit()
>для построения<proto::literal<>
>на месте. Вышеуказанное выражение можно просто записать как:
// proto::lit(0) creates an integer terminal expression proto::lit(0) + 1;
Как только у нас есть некоторые терминалы Proto, выражения, включающие эти терминалы, строят деревья экспрессии для нас. Proto определяет перегрузки для каждого из перегружаемых операторов C++ в пространстве имен<boost::proto
>. Пока один операнд является выражением Прото, результатом операции является узел дерева, представляющий эту операцию.
![]() | Note |
---|---|
Операторские перегрузки Proto живут в пространстве имен< |
В результате перегрузок оператора Proto можно сказать:
-_1; // OK, build a unary-negate tree node _1 + 42; // OK, build a binary-plus tree node
По большей части, это просто работает, и вам не нужно думать об этом, но некоторые операторы являются особенными, и может быть полезно знать, как Proto обрабатывает их.
Прото также перегружает<operator=
>,<operator[]
>и<operator()
>, но эти операторы являются функциями-членами шаблона выражения, а не свободными функциями в пространстве имён Прото. Ниже приведены действительные выражения Proto:
_1 = 5; // OK, builds a binary assign tree node _1[6]; // OK, builds a binary subscript tree node _1(); // OK, builds a unary function tree node _1(7); // OK, builds a binary function tree node _1(8,9); // OK, builds a ternary function tree node // ... etc.
Для первых двух строк, назначения и субскрипта, неудивительно, что результирующий узел выражения должен быть двоичным. В конце концов, в каждом выражении есть два операнда. Поначалу может показаться удивительным, что то, что кажется функцией вызова без аргументов<_1()
>, фактически создает узел выражения с одним ребенком. Ребенок сам по себе<_1
>. Также у выражения<_1(7)
>двое детей:<_1
>и<7
>.
Поскольку эти операторы могут быть определены только как функции-члены, следующие выражения являются недействительными:
int i; i = _1; // ERROR: cannot assign _1 to an int int *p; p[_1]; // ERROR: cannot use _1 as an index std::sin(_1); // ERROR: cannot call std::sin() with _1
Кроме того, C++ имеет специальные правила для перегрузок<operator->
>, которые делают его бесполезным для построения шаблонов выражения, поэтому Proto не перегружает его.
Proto перегружает адрес оператора для типов экспрессии, так что следующий код создает новый унарный адрес узла дерева:
&_1; // OK, creates a unary address-of tree node
Онневозвращает адрес<_1
>объекта. Однако в Proto есть специальный код, так что унарный адрес узла неявно конвертируется в указатель на его ребенка. Другими словами, следующий код работает и делает то, что вы можете ожидать, но не очевидным образом:
typedef proto::terminal< placeholder<0> >::type _1_type; _1_type const _1 = {{}}; _1_type const * p = &_1; // OK, &_1 implicitly converted
Если бы мы ограничивались только терминалами и перегрузками операторов, наши встроенные языки доменов не были бы очень выразительными. Представьте, что мы хотели расширить наш калькулятор EDSL с полным набором математических функций, таких как<sin()
>и<pow()
>, которые мы могли бы лениво вызвать следующим образом.
// A calculator expression that takes one argument // and takes the sine of it. sin(_1);
Мы хотели бы, чтобы вышеприведенное создало шаблон выражения, представляющий вызов функции. Когда это выражение оценивается, оно должно вызывать функцию. (По крайней мере, в этом смысл вызова функции, который мы хотели бы иметь в калькуляторе EDSL.) Вы можете определить<sin
>следующим образом:
// "sin" is a Proto terminal containing a function pointer proto::terminal< double(*)(double) >::type const sin = {&std::sin};
В приведенном выше описании мы определяем<sin
>как терминал Proto, содержащий указатель на функцию<std::sin()
>. Теперь мы можем использовать<sin
>в качестве ленивой функции.<default_context
>, который мы видели во Введении, знает, как оценивать ленивые функции. Рассмотрим следующее:
double pi = 3.1415926535; proto::default_context ctx; // Create a lazy "sin" invocation and immediately evaluate it std::cout << proto::eval( sin(pi/2), ctx ) << std::endl;
Вышеприведенный код распечатывает:
1
Я не специалист по тригонометрии, но мне это кажется правильным.
Мы можем написать<sin(pi/2)
>, потому что<sin
>объект, который является терминалом Proto, имеет перегруженный<operator()()
>, который строит узел, представляющий функцию вызова вызова. Тип<sin(pi/2)
>на самом деле таков:
// The type of the expression sin(pi/2): proto::function< proto::terminal< double(*)(double) >::type const & proto::result_of::as_child< double const >::type >::type
Этот тип дополнительно расширяется до неприглядного типа узла с тегомтипа<proto::tag::function
>и двумя детьми: первый представляет функцию, на которую следует ссылаться, а второй представляет аргумент функции. (Таги узлов описывают операцию, которая создала узел.) Разница между<a
+b
>и<a-
b
>заключается в том, что первый имеет тип тега<proto::tag::plus
>, а второй имеет тип тега<proto::tag::minus
>. Типы тегов - это чистая информация о времени компиляции.
![]() | Note |
---|---|
При вычислении типов< |
Важно отметить, что в терминалах, содержащих указатели функций, нет ничего особенного.ЛюбойВыражение прото имеет перегруженную функцию оператора вызова. Подумайте:
// This compiles! proto::lit(1)(2)(3,4)(5,6,7,8);
Сначала это может показаться странным. Он создает целочисленный терминал с<proto::lit()
>, а затем снова и снова вызывает его как функцию. Что это значит? Кто знает?! Вы можете решить, когда вы определяете контекст оценки или преобразование. Но об этом позже.
Что, если мы хотим добавить функцию<pow()
>в наш калькулятор EDSL, которую пользователи могли бы вызвать следующим образом?
// A calculator expression that takes one argument // and raises it to the 2nd power pow< 2 >(_1);
Описанная выше простая техника создания<pow
>терминала, содержащего указатель функции, здесь не работает. Если<pow
>является объектом, то выражение<pow<
2>(_1)
>не является действительным C++. (Ну, технически это так; это означает<pow
>меньше, чем 2, больше, чем<(_1)
>, что совсем не похоже на то, что мы хотим.)<pow
>должен быть реальным шаблоном функций. Но это должна быть необычная функция: та, которая возвращает шаблон выражения.
С<sin
>мы полагались на Proto, чтобы обеспечить перегруженный<operator()()
>, чтобы построить узел выражения с типом тега<proto::tag::function
>для нас. Теперь мы должны сделать это сами. Как и раньше, у узла будет двое детей: функция вызова и аргумент функции.
С<sin
>функция вызова была необработанным указателем функции, завернутым в терминал Proto. В случае<pow
>мы хотим, чтобы это был терминал, содержащий объект функции в стиле TR1. Это позволит нам параметризировать функцию на экспоненте. Ниже приведена реализация простой обертки в стиле TR1 для функции<std::pow
>:
// Define a pow_fun function object template< int Exp > struct pow_fun { typedef double result_type; double operator()(double d) const { return std::pow(d, Exp); } };
Следуя примеру<sin
>, мы хотим<pow<
1>(
pi/2)
>иметь такой тип:
// The type of the expression pow<1>(pi/2): proto::function< proto::terminal< pow_fun<1> >::type proto::result_of::as_child< double const >::type >::type
Мы могли бы написать функцию<pow()
>, используя такой код, но она многословна и подвержена ошибкам; слишком легко ввести тонкие ошибки, забыв позвонить<proto::as_child()
>, где это необходимо, в результате чего код, который, кажется, работает, но иногда не работает. Proto обеспечивает лучший способ построения экспрессионных узлов:<proto::make_expr()
>.
make_expr()
Proto помогает создавать шаблоны выражения под названием<proto::make_expr()
>. Мы можем кратко определить функцию<pow()
>с ней как ниже.
// Define a lazy pow() function for the calculator EDSL. // Can be used as: pow< 2 >(_1) template< int Exp, typename Arg > typename proto::result_of::make_expr< proto::tag::function // Tag type , pow_fun< Exp > // First child (by value) , Arg const & // Second child (by reference) >::type const pow(Arg const &arg) { return proto::make_expr<proto::tag::function>( pow_fun<Exp>() // First child (by value) , boost::ref(arg) // Second child (by reference) ); }
Есть некоторые вещи, которые следует отметить в вышеупомянутом коде. Мы используем<proto::result_of::make_expr<>
>для расчета типа возврата. Первый параметр шаблона - это тип тега для узла выражения, который мы строим - в данном случае<proto::tag::function
>.
Последующие параметры шаблона<proto::result_of::make_expr<>
>представляют собой дочерние узлы. Если тип ребенка еще не является выражением Прото, он автоматически превращается в терминал с<proto::as_child()
>. Тип, такой как<pow_fun<Exp>
>, приводит к терминалу, который удерживается значением, тогда как тип, такой как<Arg
const&
>(обратите внимание на ссылку), указывает, что результат должен удерживаться ссылкой.
В теле функции находится вызов времени выполнения<proto::make_expr()
>. Это близко отражает расчет типа возврата.<proto::make_expr()
>требует, чтобы вы указали тип тега узла в качестве параметра шаблона. Аргументами функции становятся дети узла. Когда ребенка нужно хранить по ценности, ничего особенного делать не надо. Когда ребенок должен храниться по ссылке, вы должны использовать функцию<boost::ref()
>, чтобы обернуть аргумент.
И это все!<proto::make_expr()
>- это способ ленивого человека сделать ленивую забаву.
В этом разделе мы узнаем все одоменах. В частности, мы узнаем:
В разделеHello Calculatorмы рассмотрели возможность непосредственного использования выражений калькулятора в качестве лямбда-выражений в вызовах алгоритмов STL, как показано ниже:
double data[] = {1., 2., 3., 4.}; // Use the calculator EDSL to square each element ... HOW? std::transform( data, data + 4, data, _1 * _1 );
Сложность, если вы помните, заключалась в том, что по умолчанию выражения Proto не имеют собственного интересного поведения. Это просто деревья. В частности, выражение<_1
*_1
>не будет иметь<operator()
>, которое принимает двойной и возвращает двойной, как<std::transform()
>ожидает, если мы не дадим ему один. Чтобы сделать эту работу, нам нужно было определить тип обертки экспрессии, который определял функцию члена<operator()
>, и нам нужно было связать обертку с калькулятором.
В Прото терминдоменотносится к типу, который связывает выражения в этом домене с экспрессиейгенератора. Генератор — это просто функциональный объект, который принимает выражение и делает с ним что-то, например, заворачивает его в обертку для выражения.
Вы также можете использовать домен, чтобы связать выражения с грамматикой. Когда вы указываете грамматику домена, Proto гарантирует, что все выражения, которые он генерирует в этом домене, соответствуют грамматике домена. Он делает это, отключая любые перегрузки оператора, которые создают недействительные выражения.
Первый шаг к тому, чтобы дать калькулятору дополнительные выражения поведения, - это определить область калькулятора. Все выражения в области калькулятора будут проникнуты калькуляторностью, как мы увидим.
// A type to be used as a domain tag (to be defined below) struct calculator_domain;
Мы используем этот тип домена при расширении типа<proto::expr<>
>, что мы делаем с шаблоном класса<proto::extends<>
>. Вот наша обертка для выражения, которая наполняет выражение калькулятором. Это описано ниже.
// The calculator<> expression wrapper makes expressions // function objects. template< typename Expr > struct calculator : proto::extends< Expr, calculator< Expr >, calculator_domain > { typedef proto::extends< Expr, calculator< Expr >, calculator_domain > base_type; calculator( Expr const &expr = Expr() ) : base_type( expr ) {} // This is usually needed because by default, the compiler- // generated assignment operator hides extends<>::operator= BOOST_PROTO_EXTENDS_USING_ASSIGN(calculator) typedef double result_type; // Hide base_type::operator() by defining our own which // evaluates the calculator expression with a calculator context. result_type operator()( double d1 = 0.0, double d2 = 0.0 ) const { // As defined in the Hello Calculator section. calculator_context ctx; // ctx.args is a vector<double> that holds the values // with which we replace the placeholders (e.g., _1 and _2) // in the expression. ctx.args.push_back( d1 ); // _1 gets the value of d1 ctx.args.push_back( d2 ); // _2 gets the value of d2 return proto::eval(*this, ctx ); // evaluate the expression } };
Мы хотим, чтобы выражения калькулятора были функциональными объектами, поэтому мы должны определить<operator()
>, который принимает и возвращает удваивается. С помощью<proto::extends<>
>обертки выше это делает<calculator<>
>шаблон. Первый шаблон к<proto::extends<>
>параметру — это тип выражения, который мы расширяем. Второй — это тип завернутого выражения. Третий параметр — это домен, с которым связана эта обертка. Тип обертки, такой как<calculator<>
>, который наследует от<proto::extends<>
>, ведет себя так же, как тип выражения, который он расширил, с любым дополнительным поведением, которое вы решите дать ему.
![]() | Note |
---|---|
Почему бы не наследовать от< Вы можете подумать, что этот бизнес по расширению выражения излишне сложен. Почему C++ поддерживает наследование? Почему< |
Хотя это и не является строго необходимым в данном случае, мы вводим<extends<>::operator=
>в область применения макроса<BOOST_PROTO_EXTENDS_USING_ASSIGN()
>. Это действительно необходимо, только если вы хотите, чтобы такие выражения, как<_1
=3
>, создавали лениво оцененное задание.<proto::extends<>
>определяет подходящий<operator=
>для вас, но созданный компилятором<calculator<>::operator=
>будет скрывать его, если вы не сделаете его доступным с помощью макроса.
Обратите внимание, что при реализации<calculator<>::operator()
>мы оцениваем выражение с<calculator_context
>, которое мы определили ранее. Как мы видели ранее, контекст — это то, что придает операторам их значение. В случае калькулятора контекст также определяет значение терминалов заполнителя.
Теперь, когда мы определили экспрессионную обертку<calculator<>
>, нам нужно обернуть заполнители, чтобы наполнить их калькулятором:
calculator< proto::terminal< placeholder<0> >::type > const _1; calculator< proto::terminal< placeholder<1> >::type > const _2;
BOOST_PROTO_EXTENDS()
Чтобы использовать<proto::extends<>
>, ваш тип расширения должен исходить из<proto::extends<>
>. К сожалению, это означает, что ваш тип расширения больше не является POD, и его экземпляры не могут бытьстатически инициализированы. (См. разделСтатическая инициализацияв приложенииОбоснование, почему это важно.) В частности, как определено выше, глобальные объекты-заполнители<_1
>и<_2
>должны быть инициализированы во время выполнения, что может привести к тонкому порядку ошибок инициализации.
Есть еще один способ сделать расширение выражения, которое не приносит в жертву POD-несущность: макрос<
. Вы можете использовать его так же, как вы используете<BOOST_PROTO_EXTENDS
>proto::extends<>
>. Мы можем использовать<
, чтобы сохранить<BOOST_PROTO_EXTENDS
>()calculator<>
>POD и наши заполнители статически инициализированы.
// The calculator<> expression wrapper makes expressions // function objects. template< typename Expr > struct calculator { // Use BOOST_PROTO_EXTENDS() instead of proto::extends<> to // make this type a Proto expression extension. BOOST_PROTO_EXTENDS(Expr, calculator<Expr>, calculator_domain) typedef double result_type; result_type operator()( double d1 = 0.0, double d2 = 0.0 ) const { /* ... as before ... */ } };
С новым типом<calculator<>
>мы можем переопределить наши заполнители для статической инициализации:
calculator< proto::terminal< placeholder<0> >::type > const _1 = {{{}}}; calculator< proto::terminal< placeholder<1> >::type > const _2 = {{{}}};
Нам нужно внести еще одно небольшое изменение, чтобы приспособиться к POD-состоянию нашего расширения экспрессии, которое мы опишем ниже в разделе о генераторах экспрессии.
Что делает<
? Он определяет элемент данных расширяемого типа выражения; некоторые вложенные типдефы, которые требуются Proto;<BOOST_PROTO_EXTENDS
>operator=
>,<operator[]
>и<operator()
>перегрузки для построения шаблонов выражения; и вложенный<result<>
>шаблон для расчета типа возврата<operator()
>. Однако в этом случае перегрузки<operator()
>и шаблон<result<>
>не нужны, потому что мы определяем наш собственный<operator()
>в<calculator<>
>типе. Proto предоставляет дополнительные макросы для более точного контроля над функциями членов. Мы могли бы улучшить наш<calculator<>
>тип следующим образом:
// The calculator<> expression wrapper makes expressions // function objects. template< typename Expr > struct calculator { // Use BOOST_PROTO_BASIC_EXTENDS() instead of proto::extends<> to // make this type a Proto expression extension: BOOST_PROTO_BASIC_EXTENDS(Expr, calculator<Expr>, calculator_domain) // Define operator[] to build expression templates: BOOST_PROTO_EXTENDS_SUBSCRIPT() // Define operator= to build expression templates: BOOST_PROTO_EXTENDS_ASSIGN() typedef double result_type; result_type operator()( double d1 = 0.0, double d2 = 0.0 ) const { /* ... as before ... */ } };
Обратите внимание, что теперь мы используем<
вместо<BOOST_PROTO_BASIC_EXTENDS
>()
. Это просто добавляет элемент данных и вложенные типдефы, но не любой из перегруженных операторов. Они добавляются отдельно<BOOST_PROTO_EXTENDS
>()
и<BOOST_PROTO_EXTENDS_ASSIGN
>()
. Мы исключаем оператор вызова функций и вложенный шаблон<BOOST_PROTO_EXTENDS_SUBSCRIPT
>()result<>
>, который мог быть определен с макросом Proto<BOOST_PROTO_EXTENDS_FUNCTION
>() [316).
Итак, вот макросы, которые вы можете использовать для определения расширений выражений, и краткое описание каждого из них.
Table 29.2. Expression Extension Macros
Макро |
Цель |
---|---|
| Определяет элемент данных типа< |
Определяет< | |
Определяет< | |
Определяет< | |
|
Equivalent to:
|
![]() | Warning |
---|---|
Аргументно-зависимый поиск и< Перегрузки оператора Proto определяются в пространстве имен< template<class T> struct my_complex { BOOST_PROTO_EXTENDS( typename proto::terminal<std::complex<T> >::type , my_complex<T> , proto::default_domain ) }; int main() { my_complex<int> c0, c1; c0 + c1; // ERROR: operator+ not found } Проблема связана с тем, как работает аргументированный поиск. Тип< Так что мы можем сделать? Добавив дополнительный параметр шаблона, который по умолчанию соответствует типу в пространстве имен< template<class T, class Dummy = proto::is_proto_expr> struct my_complex { BOOST_PROTO_EXTENDS( typename proto::terminal<std::complex<T> >::type , my_complex<T> , proto::default_domain ) }; int main() { my_complex<int> c0, c1; c0 + c1; // OK, operator+ found now! } Тип< |
Последнее, что еще предстоит сделать, это сказать Прото, что он должен обернуть все наши выражения калькулятора в нашу обертку<calculator<>
>. Мы уже завернули заполнители, но мы хотим, чтобы всевыражения, которые включают заполнители калькулятора, были калькуляторами. Мы можем сделать это, указав генератор экспрессии, когда мы определяем наш<calculator_domain
>следующим образом:
// Define the calculator_domain we forward-declared above. // Specify that all expression in this domain should be wrapped // in the calculator<> expression wrapper. struct calculator_domain : proto::domain< proto::generator< calculator > > {};
Первым параметром шаблона<proto::domain<>
>является генератор. «Генератор» — это просто причудливое название функционального объекта, который принимает выражение и что-то с ним делает.<proto::generator<>
>очень простой — он заворачивает выражение в обертку, которую вы указываете.<proto::domain<>
>наследует от своего параметра генератора, поэтому все домены сами являются функциональными объектами.
Если мы используем<
для сохранения нашего типа расширения экспрессии POD, то нам нужно использовать<BOOST_PROTO_EXTENDS
>()proto::pod_generator<>
>вместо<proto::generator<>
>следующим образом:
// If calculator<> uses BOOST_PROTO_EXTENDS() instead of // use proto::extends<>, use proto::pod_generator<> instead // of proto::generator<>. struct calculator_domain : proto::domain< proto::pod_generator< calculator > > {};
После того, как Proto вычислил новый тип выражения, он проверяет домены детских выражений. Они должны совпадать. Предполагая, что они это сделают, Proto создает новое выражение и передает его<
для любой дополнительной обработки. Если мы не укажем генератор, новое выражение будет пропущено без изменений. Но поскольку мы указали генератор выше,<Domain
>::оператор[]calculator_domain::operator()
>возвращает<calculator<>
>объектов.
Теперь мы можем использовать выражения калькулятора в качестве объектов функций для алгоритмов STL следующим образом:
double data[] = {1., 2., 3., 4.}; // Use the calculator EDSL to square each element ... WORKS! :-) std::transform( data, data + 4, data, _1 * _1 );
По умолчанию Proto определяет все возможные операторские перегрузки для Protofied-выражений. Это позволяет легко объединить EDSL. В некоторых случаях, однако, наличие беспорядочных перегрузок Прото может привести к путанице или еще хуже. Когда это произойдет, вам придется отключить некоторых перегруженных операторов Proto. Это делается путем определения грамматики для вашего домена и указания ее в качестве второго параметра шаблона<proto::domain<>
>.
В разделеHello Calculatorмы увидели пример грамматики Прото, который повторяется здесь:
// Define the grammar of calculator expressions struct calculator_grammar : proto::or_< proto::plus< calculator_grammar, calculator_grammar > , proto::minus< calculator_grammar, calculator_grammar > , proto::multiplies< calculator_grammar, calculator_grammar > , proto::divides< calculator_grammar, calculator_grammar > , proto::terminal< proto::_ > > {};
У нас будет гораздо больше информации о грамматике в последующих разделах, но пока мы просто скажем, что структура<calculator_grammar
>описывает подмножество всех типов выражений — подмножество, которое содержит действительные выражения калькулятора. Мы хотели бы запретить Прото создавать калькуляторное выражение, не соответствующее этой грамматике. Мы делаем это, изменяя определение структуры<calculator_domain
>.
// Define the calculator_domain. Expressions in the calculator // domain are wrapped in the calculator<> wrapper, and they must // conform to the calculator_grammar: struct calculator_domain : proto::domain< proto::generator< calculator >, calculator_grammar > {};
Единственным новым дополнением является<calculator_grammar
>в качестве второго параметра шаблона<proto::domain<>
>. Это приводит к отключению любой из перегрузок оператора Proto, что создает недействительное выражение калькулятора.
Другим распространенным использованием этой функции будет отключение унарной перегрузки Proto<operator&
>. Для пользователей EDSL может быть удивительно, что они не могут взять адрес своего выражения! Вы можете очень легко отключить перегрузку Proto<operator&
>для вашего домена с помощью очень простой грамматики, как показано ниже:
// For expressions in my_domain, disable Proto's // unary address-of operator. struct my_domain : proto::domain< proto::generator< my_wrapper > // A simple grammar that matches any expression that // is not a unary address-of expression. , proto::not_< proto::address_of< _ > > > {};
Тип<proto::not_<
proto::address_of<
_>
>
>— очень простая грамматика, которая соответствует всем выражениям, кроме унарных адресов выражений. В разделе, описывающем промежуточную форму Прото, мы расскажем гораздо больше о грамматике.
![]() | Note |
---|---|
Это продвинутая тема. Не стесняйтесь пропустить это, если вы только начинаете с Proto. |
Оператор Proto перегружает сборку выражений из подвыражений. Суб-выражения становятся детьми нового выражения. По умолчанию дети хранятся у родителя по ссылке. В этом разделе описывается, как изменить этот дефолт.
as_child
vs. as_expr
Прото позволяет самостоятельно настраивать поведение<proto::as_child()
>и<proto::as_expr()
>. Оба принимают объект<x
>и возвращают выражение Прото, превращая его<x
>в терминал Прото, если это необходимо. Хотя эти две функции похожи, они используются в разных ситуациях и по умолчанию имеют разное поведение. Важно понимать разницу, чтобы вы знали, что настроить для достижения желаемого поведения.
<proto::as_expr()
>обычно используетсявы, чтобы превратить объект в выражение Прото, которое должно содержаться в локальной переменной, так как:
auto l = proto::as_expr(x); // Turn x into a Proto expression, hold the result in a local
Вышеизложенное работает независимо от того, является ли<x
>уже выражением Прото или нет. Объект<l
>гарантированно является действительным выражением Прото. Если<x
>является непротообъектом, он превращается в терминальное выражение, которое удерживает<x
>по значению.<#include
<boost/proto/core.hpp>
>Если<x
>уже является объектом Прото,<proto::as_expr()
>возвращает егопо значениюнеизмененным.
Напротив,<proto::as_child()
>используется внутри Proto для предварительной обработки объектов, прежде чем сделать их детьми другого выражения. Поскольку он является внутренним для Proto, вы не видите его явно, но он есть за кулисами в таких выражениях:
x + y; // Consider that y is a Proto expression, but x may or may not be.
В этом случае Proto строит узел плюс от двух детей. Оба предварительно обрабатываются, передавая их<proto::as_child()
>, прежде чем сделать их детьми нового узла. Если<x
>не является выражением Прото, оно становится таковым, будучи завернутым в терминал Прото, который удерживает егопосредством ссылки. Если<x
>уже является выражением Прото,<proto::as_child()
>возвращает егопосредством ссылкинеизмененным. Сравните это с приведенным выше описанием<proto::as_expr()
>.
В приведенной ниже таблице кратко изложено вышеизложенное описание.
Table 29.3. proto::as_expr() vs. proto::as_child()
Функция |
454Когда< |
Когда< |
---|---|---|
< | Возврат (по стоимости) нового держателя терминала Proto< | Возврат< |
< | Возврат (по стоимости) нового держателя терминала Proto< |
![]() | Note |
---|---|
Есть одно важное место, где Прото использует< |
Теперь, когда вы знаете, что такое<proto::as_child()
>и<proto::as_expr()
>, где они используются и что они делают по умолчанию, вы можете решить, что одна или обе эти функции должны иметь различное поведение для вашего домена. Например, учитывая приведенное выше описание<proto::as_child()
>, следующий код всегда неверен:
proto::literal<int> i(0); auto l = i + 42; // This is WRONG! Don't do this.
Почему это неправильно? Потому что<proto::as_child()
>превратит целое число 42 в терминал Прото, который содержит ссылку на временное целое число, инициализированное с 42. Время жизни этого временного числа заканчивается в полуколоне, гарантируя, что местный<l
>останется с болтающейся ссылкой на целое число умерших. Что делать? Один из ответов — использовать<proto::deep_copy()
>. Другой способ — настроить поведение<proto::as_child()
>для вашего домена. Читайте дальше для деталей.
as_child
Чтобы контролировать, как Proto строит выражения из подвыражений в вашем домене, определите свой домен как обычно, а затем определите вложенный шаблон класса<as_child<>
>в нем следующим образом:
class my_domain : proto::domain< my_generator, my_grammar > { // Here is where you define how Proto should handle // sub-expressions that are about to be glommed into // a larger expression. template< typename T > struct as_child { typedefunspecified-Proto-expr-type
result_type; result_type operator()( T & t ) const { returnunspecified-Proto-expr-object
; } }; };
Следует отметить одну важную вещь: в приведенном выше коде параметр шаблона<T
>может быть или не быть типом выражения Proto, но результатдолженбыть типом выражения Proto или ссылкой на него. Это означает, что большинство шаблонов, определяемых пользователем<as_child<>
>, должны проверить, является ли<T
>выражением или нет (с использованием<proto::is_expr<>
>), а затем превратить невыражения в терминалы Proto, обернув их как<proto::terminal</* ... */
>::type
>или эквивалент.
as_expr
Хотя это менее распространено, Proto также позволяет настраивать поведение<proto::as_expr()
>на основе каждого домена. Техника идентична таковой для<as_child
>. См. ниже:
class my_domain : proto::domain< my_generator, my_grammar > { // Here is where you define how Proto should handle // objects that are to be turned into expressions // fit for storage in local variables. template< typename T > struct as_expr { typedefunspecified-Proto-expr-type
result_type; result_type operator()( T & t ) const { returnunspecified-Proto-expr-object
; } }; };
auto
-safeДавайте еще раз рассмотрим проблему, описанную выше, связанную с ключевым словом C++11<auto
>и поведением по умолчанию<proto::as_child()
>.
proto::literal<int> i(0); auto l = i + 42; // This is WRONG! Don't do this.
Напомним, что проблема заключается в сроке службы временного целого числа, созданного для удержания значения 42. Местный<l
>останется с намеком на него после того, как его жизнь закончится. Что, если мы хотим, чтобы Proto делал выражения безопасными для хранения в локальных переменных? Мы можем сделать это очень легко, заставив<proto::as_child()
>вести себя так же, как<proto::as_expr()
>. Это достигается следующим кодом:
template< typename E > struct my_expr; struct my_generator : proto::pod_generator< my_expr > {}; struct my_domain : proto::domain< my_generator > { // Make as_child() behave like as_expr() in my_domain. // (proto_base_domain is a typedef for proto::domain< my_generator > // that is defined in proto::domain<>.) template< typename T > struct as_child : proto_base_domain::as_expr< T > {}; }; template< typename E > struct my_expr { BOOST_PROTO_EXTENDS( E, my_expr< E >, my_domain ) }; /* ... */ proto::literal< int, my_domain > i(0); auto l = i + 42; // OK! Everything is stored by value here.
Обратите внимание, что<my_domain::as_child<>
>просто откладывается от реализации по умолчанию<as_expr<>
>, найденной в<proto::domain<>
>. Путем простого перекрестного подключения нашего домена<as_child<>
>к<as_expr<>
>мы гарантируем, что все терминалы, которые могут удерживаться стоимостью, являются, и что все выражения ребенка также удерживаются стоимостью. Это увеличивает копирование и может повлечь за собой затраты на производительность во время выполнения, но это устраняет любые проблемы управления сроком службы.
В другом примере см. определение<lldomain
>в<libs/proto/example/lambda.hpp
>. Этот пример является полным повторением Библиотеки Boost Lambda (BLL) на вершине Boost. Объекты функции, генерируемые BLL, безопасны для хранения в локальных переменных. Чтобы эмулировать это с помощью Proto,<lldomain
>кросс-провода<as_child<>
>до<as_expr<>
>, как указано выше, но с одним дополнительным поворотом: объекты с типом массива также хранятся по ссылке. Посмотри.
![]() | Note |
---|---|
Это продвинутая тема. Не стесняйтесь пропустить это, если вы только начинаете с Proto. |
Способностьсоставлятьразличные EDSL является одной из их самых захватывающих особенностей. Подумайте, как вы строите парсер с использованием yacc. Вы пишете свои правила грамматики на доменном языке yacc. Затем вы встраиваете семантические действия, написанные на C, в свою грамматику. Парсерный генератор Boost Spirit дает вам такую же возможность. Вы пишете грамматические правила, используя Дух. Ци и встраивание семантических действий с помощью библиотеки Феникса. Phoenix и Spirit являются прото-ориентированными доменными языками со своим собственным синтаксисом и семантикой. Но вы можете свободно встраивать выражения Феникса в выражения Духа. В этом разделе описана функция поддомена Proto, которая позволяет определять семейства взаимодействующих доменов.
Когда вы пытаетесь создать выражение из двух подвыражений в разных доменах, что такое домен полученного выражения? Это основная проблема, которая решается поддоменами. Рассмотрим следующий код:
#include <boost/proto/proto.hpp> namespace proto = boost::proto; // Forward-declare two expression wrappers template<typename E> struct spirit_expr; template<typename E> struct phoenix_expr; // Define two domains struct spirit_domain : proto::domain<proto::generator<spirit_expr> > {}; struct phoenix_domain : proto::domain<proto::generator<phoenix_expr> > {}; // Implement the two expression wrappers template<typename E> struct spirit_expr : proto::extends<E, spirit_expr<E>, spirit_domain> { spirit_expr(E const &e = E()) : spirit_expr::proto_extends(e) {} }; template<typename E> struct phoenix_expr : proto::extends<E, phoenix_expr<E>, phoenix_domain> { phoenix_expr(E const &e = E()) : phoenix_expr::proto_extends(e) {} }; int main() { proto::literal<int, spirit_domain> sp(0); proto::literal<int, phoenix_domain> phx(0); // Whoops! What does it mean to add two expressions in different domains? sp + phx; // ERROR }
Выше мы определяем два домена, называемые<spirit_domain
>и<phoenix_domain
>, и объявляем два буквальных обозначения в каждом. Затем мы пытаемся составить их в более крупное выражение с помощью оператора двоичного плюса Proto, и это не удается. Прото не может понять, должно ли полученное выражение быть в области Духа или в области Феникса, и, таким образом, должно ли оно быть примером<spirit_expr<>
>или<phoenix_expr<>
>. Мы должны рассказать Прото, как разрешить конфликт. Мы можем сделать это, объявив, что Феникс является поддоменом Духа, как в следующем определении<phoenix_domain
>:
// Declare that phoenix_domain is a sub-domain of spirit_domain struct phoenix_domain : proto::domain<proto::generator<phoenix_expr>, proto::_, spirit_domain> {};
Третий параметр шаблона<proto::domain<>
>— супердомен. Определяя<phoenix_domain
>выше, мы говорим, что выражения Феникса могут быть объединены с выражениями Духа, и что, когда это происходит, результирующее выражение должно быть выражением Духа.
![]() | Note |
---|---|
Если вам интересно, какова цель< |
Когда в заданном выражении задействовано несколько доменов, Proto использует некоторые правила, чтобы выяснить, какой домен «выигрывает». Правила слабо смоделированы на правилах наследования C++.<Phoenix_domain
>является поддоменом<spirit_domain
>. Вы можете сравнить это с производными / базовыми отношениями, которые придают выражениям Феникса неявное преобразование в выражения Духа. И поскольку выражения Феникса могут быть «превращены» в выражения Духа, они могут свободно сочетаться с выражениями Духа, и результатом является выражение Духа.
![]() | Note |
---|---|
Супер- и поддомены фактически не реализуются с использованием наследования. Это только полезная ментальная модель. |
Аналогия с наследованием имеет место даже в случае трех доменов, когда два являются поддоменами третьего. Представьте себе другой домен под названием<foobar_domain
>, который также был поддоменом<spirit_domain
>. Выражения в<foobar_domain
>могут быть объединены с выражениями в<phoenix_domain
>, и результирующее выражение будет в<spirit_domain
>. Это потому, что выражения в двух поддоменах имеют «конверсии» в супердомен, поэтому операция разрешена, и супердомен выигрывает.
Когда вы не присваиваете выражение Proto определенному домену, Proto считает его членом так называемого домена по умолчанию<proto::default_domain
>. Даже объекты, не являющиеся Proto, рассматриваются как терминалы в домене по умолчанию. Подумайте:
int main() { proto::literal<int, spirit_domain> sp(0); // Add 1 to a spirit expression. Result is a spirit expression. sp + 1; }
Выражения в домене по умолчанию (или невыражения, такие как<1
>) имеют своего рода неявное преобразование в выражения любого другого типа домена. Более того, вы можете определить свой домен как поддомен домена по умолчанию. При этом вы даете выражения в ваших конверсиях домена в выражения в любом другом домене. Это как& #8220;свободная любовь& #8221;домен, потому что он будет свободно смешиваться со всеми другими доменами.
Давайте еще раз подумаем о Phoenix EDSL. Поскольку он обеспечивает в целом полезную лямбда-функциональность, разумно предположить, что многие другие EDSL, помимо Spirit, могут захотеть встроить выражения Phoenix. Иными словами,<phoenix_domain
>должно быть поддоменом<proto::default_domain
>, а не<spirit_domain
>:
// Declare that phoenix_domain is a sub-domain of proto::default_domain struct phoenix_domain : proto::domain<proto::generator<phoenix_expr>, proto::_, proto::default_domain> {};
Это намного лучше. Выражения Феникса теперь можно ставить где угодно.
Используйте поддомены Proto, чтобы смешивать выражения из нескольких доменов. И если вы хотите, чтобы выражения в вашем домене свободно комбинировались свсемивыражениями, сделайте его поддоменом<proto::default_domain
>.
Предыдущие обсуждения определения передних концов Proto сделали большое предположение: у вас есть роскошь определять все с нуля. Что произойдет, если у вас есть существующие типы, например, тип матрицы и векторный тип, которые вы хотели бы рассматривать как прото-терминалы? Прото обычно торгуется только в своих собственных типах выражений, но с<
, он также может размещать ваши пользовательские типы терминалов.BOOST_PROTO_DEFINE_OPERATORS
>()
Допустим, например, что у вас есть следующие типы и что вы не можете изменить их, чтобы сделать их& #8220;родные& #8221;типы терминалов Proto.
namespace math { // A matrix type ... struct matrix { /*...*/ }; // A vector type ... struct vector { /*...*/ }; }
Вы можете ненавязчиво создавать объекты этих типов терминалов Proto, определяя соответствующие операторские перегрузки с помощью<
. Основная процедура заключается в следующем:BOOST_PROTO_DEFINE_OPERATORS
>()
BOOST_PROTO_DEFINE_OPERATORS
>()
для определения набора перегрузок оператора, передав имя признака в качестве первого макропараметра и имя домена Proto (например,<proto::default_domain
>) в качестве второго.Следующий код показывает, как это работает.
namespace math { template<typename T> struct is_terminal : mpl::false_ {}; // OK, "matrix" is a custom terminal type template<> struct is_terminal<matrix> : mpl::true_ {}; // OK, "vector" is a custom terminal type template<> struct is_terminal<vector> : mpl::true_ {}; // Define all the operator overloads to construct Proto // expression templates, treating "matrix" and "vector" // objects as if they were Proto terminals. BOOST_PROTO_DEFINE_OPERATORS(is_terminal, proto::default_domain) }
Вызыв макроса<
определяет полный набор операторских перегрузок, которые обрабатывают<BOOST_PROTO_DEFINE_OPERATORS
>matrix
>и<vector
>объекты, как если бы они были терминалами Proto. И поскольку операторы определены в том же пространстве имен, что и типы<matrix
>и<vector
>, операторы будут найдены путем поиска, зависящего от аргументов. С приведенным выше кодом мы теперь можем создавать шаблоны экспрессии с матрицами и векторами, как показано ниже.
math::matrix m1; math::vector v1; proto::literal<int> i(0); m1 * 1; // custom terminal and literals are OK m1 * i; // custom terminal and Proto expressions are OK m1 * v1; // two custom terminals are OK, too.
Иногда, чтобы сделать жизнь ваших пользователей легкой, вы должны сделать свою собственную жизнь трудной. Предоставление пользователям естественного и гибкого синтаксиса часто включает в себя написание большого количества повторяющихся перегрузок функций. Этого может быть достаточно, чтобы получить повторяющиеся стрессовые травмы! Прежде чем навредить себе, ознакомьтесь с макросами, которые Proto предоставляет для автоматизации многих повторяющихся задач по генерации кода.
Представьте, что мы пишем лямбда EDSL, и мы хотели бы включить синтаксис для построения временных объектов любого типа с использованием следующего синтаксиса:
// A lambda expression that takes two arguments and // uses them to construct a temporary std::complex<> construct< std::complex<int> >( _1, _2 )
Для обсуждения представьте, что у нас уже есть шаблон объекта функции<construct_impl<>
>, который принимает аргументы и конструирует из них новые объекты. Мы хотели бы, чтобы приведенное выше выражение лямбда было эквивалентно следующему:
// The above lambda expression should be roughly equivalent // to the following: proto::make_expr<proto::tag::function>( construct_impl<std::complex<int> >() // The function to invoke lazily , boost::ref(_1) // The first argument to the function , boost::ref(_2) // The second argument to the function );
Мы можем определить наш шаблон функции<construct()
>следующим образом:
template<typename T, typename A0, typename A1> typename proto::result_of::make_expr< proto::tag::function , construct_impl<T> , A0 const & , A1 const & >::type const construct(A0 const &a0, A1 const &a1) { return proto::make_expr<proto::tag::function>( construct_impl<T>() , boost::ref(a0) , boost::ref(a1) ); }
Это работает для двух аргументов, но мы хотели бы, чтобы это работало для любого количества аргументов, вплоть до<
- 1. Почему "- 1"? Потому что один ребенок занят терминалом<BOOST_PROTO_MAX_ARITY
>.construct_impl<T>()
>, оставляя место только для<
- 1] других детей.BOOST_PROTO_MAX_ARITY
>
Для таких случаев Proto предоставляет макросы<
и<BOOST_PROTO_REPEAT
>()
. Чтобы использовать его, мы превращаем определение функции выше в макрос следующим образом:BOOST_PROTO_REPEAT_FROM_TO
>()
#define M0(N, typename_A, A_const_ref, A_const_ref_a, ref_a) \ template<typename T, typename_A(N)> \ typename proto::result_of::make_expr< \ proto::tag::function \ , construct_impl<T> \ , A_const_ref(N) \ >::type const \ construct(A_const_ref_a(N)) \ { \ return proto::make_expr<proto::tag::function>( \ construct_impl<T>() \ , ref_a(N) \ ); \ }
Обратите внимание, что мы превратили функцию в макрос, который принимает 5 аргументов. Первый — это текущий номер итерации. Остальные — названия других макросов, генерирующих разные последовательности. Например, Прото передает в качестве второго параметра имя макроса, которое будет расширяться до<typenameA0,typenameA1,...
>.
Теперь, когда мы превратили нашу функцию в макрос, мы можем передать макрос<
. Прото будет вызывать его итеративно, генерируя все перегрузки функции для нас.BOOST_PROTO_REPEAT_FROM_TO
>()
// Generate overloads of construct() that accept from // 1 to BOOST_PROTO_MAX_ARITY-1 arguments: BOOST_PROTO_REPEAT_FROM_TO(1, BOOST_PROTO_MAX_ARITY, M0) #undef M0
Как упоминалось выше, Proto передает в качестве последних 4 аргументов вашему макросу имена других макросов, которые генерируют различные последовательности. Макросы<
и<BOOST_PROTO_REPEAT
>()
выбирают по умолчанию эти параметры. Если по умолчанию не удовлетворяются ваши потребности, вы можете использовать<BOOST_PROTO_REPEAT_FROM_TO
>()
и<BOOST_PROTO_REPEAT_EX
>()
и передавать разные макросы, которые генерируют разные последовательности. Прото определяет ряд таких макросов для использования в качестве параметров к<BOOST_PROTO_REPEAT_FROM_TO_EX
>()
и<BOOST_PROTO_REPEAT_EX
>()
. Для всех подробностей ознакомьтесь с разделом<BOOST_PROTO_REPEAT_FROM_TO_EX
>()boost/proto/repeat.hpp
>.
Смотрите также<
. Он работает аналогично<BOOST_PROTO_LOCAL_ITERATE
>
и друзьям, но его легче использовать, когда вы хотите изменить один макро аргумент и принять по умолчанию для других.BOOST_PROTO_REPEAT
>
Теперь вы знаете немного о том, как создать интерфейс для своего «компилятора» EDSL - вы можете определить терминалы и функции, которые генерируют шаблоны выражения. Но мы ничего не сказали о самих шаблонах выражения. Как они выглядят? Что вы можете с ними сделать? В этом разделе мы увидим.
expr<>
Type
Все выражения Прото являются инстанциацией шаблона под названием<proto::expr<>
>(или оберткой вокруг такой инстанциации). Когда мы определяем терминал как ниже, мы действительно инициализируем экземпляр шаблона<proto::expr<>
>.
// Define a placeholder type template<int I> struct placeholder {}; // Define the Protofied placeholder terminal proto::terminal< placeholder<0> >::type const _1 = {{}};
Тип<_1
>выглядит следующим образом:
proto::expr< proto::tag::terminal, proto::term< placeholder<0> >, 0 >
Шаблон<proto::expr<>
>является наиболее важным типом в Proto. Хотя вам редко придется иметь дело с ним напрямую, он всегда находится за кулисами, держа деревья выражения вместе. На самом деле<proto::expr<>
>являетсядеревом выражения — ветви, листья и все.
Шаблон<proto::expr<>
>образует узлы в деревьях выражения. Первым параметром шаблона является тип узла; в этом случае<proto::tag::terminal
>. Это означает, что<_1
>является лиственным узлом в дереве выражения. Второй параметр шаблона представляет собой список типов детей или, в случае терминалов, тип значений терминала. Терминалы всегда имеют только один тип в списке типов. Последний параметр — это ловкость выражения. Терминалы имеют arity 0, унарные выражения имеют arity 1, и т.д.
Структура<proto::expr<>
>определяется следующим образом:
template< typename Tag, typename Args, long Arity = Args::arity > struct expr; template< typename Tag, typename Args > struct expr< Tag, Args, 1 > { typedef typename Args::child0 proto_child0; proto_child0 child0; // ... };
Структура<proto::expr<>
>не определяет конструктор или что-либо еще, что предотвратило бы статическую инициализацию. Все<proto::expr<>
>объекты инициализируются с использованиемагрегированной инициализации, с кудрявыми брекетами. В нашем примере<_1
>инициализируется инициализатором<{{}}
>. Внешние брекеты являются инициализатором для структуры<proto::expr<>
>, а внутренние брекеты - для элемента<_1.child0
>, который имеет тип<placeholder<0>
>. Обратите внимание, что мы используем брекеты для инициализации<_1.child0
>, потому что<placeholder<0>
>также является агрегатом.
Узел<_1
>является инстанциацией<proto::expr<>
>, а выражения, содержащие<_1
>, также являются инстанциациями<proto::expr<>
>. Чтобы эффективно использовать Proto, вам не придется беспокоиться о реальных типах, которые генерирует Proto. Это детали, но вы, вероятно, столкнетесь с этими типами в сообщениях об ошибках компилятора, поэтому полезно ознакомиться с ними. Типы выглядят так:
// The type of the expression -_1 typedef proto::expr< proto::tag::negate , proto::list1< proto::expr< proto::tag::terminal , proto::term< placeholder<0> > , 0 > const & > , 1 > negate_placeholder_type; negate_placeholder_type x = -_1; // The type of the expression _1 + 42 typedef proto::expr< proto::tag::plus , proto::list2< proto::expr< proto::tag::terminal , proto::term< placeholder<0> > , 0 > const & , proto::expr< proto::tag::terminal , proto::term< int const & > , 0 > > , 2 > placeholder_plus_int_type; placeholder_plus_int_type y = _1 + 42;
Есть несколько вещей, чтобы отметить об этих типах:
expr<>
>терминальные объекты. Эти новые обертки сами по себе не удерживаются ссылкой, но объект, обернутый, является. Обратите внимание, что тип Protofied<42
>буквально<intconst
&
>- удерживается ссылкой.Типы дают понять: все в дереве экспрессии Прото удерживается ссылкой. Это означает, что строительство дерева экспрессии исключительно дешево. Это вообще не предполагает копирования.
![]() | Note |
---|---|
Проницательный читатель заметит, что объект< |
Собрав выражение в дерево, вы, естественно, захотите сделать обратное и получить доступ к детям узла. Вы даже можете захотеть повторить над детьми с помощью алгоритмов от Boost. Библиотека Fusion. В этом разделе показано как.
Каждый узел в дереве экспрессии имеет как тип тега, который описывает узел, так иarity, соответствующий количеству дочерних узлов, которые он имеет. Вы можете использовать<proto::tag_of<>
>и<proto::arity_of<>
>метафункции для их извлечения. Рассмотрим следующее:
template<typename Expr> void check_plus_node(Expr const &) { // Assert that the tag type is proto::tag::plus BOOST_STATIC_ASSERT(( boost::is_same< typename proto::tag_of<Expr>::type , proto::tag::plus >::value )); // Assert that the arity is 2 BOOST_STATIC_ASSERT( proto::arity_of<Expr>::value == 2 ); } // Create a binary plus node and use check_plus_node() // to verify its tag type and arity: check_plus_node( proto::lit(1) + 2 );
Для данного типа<Expr
>вы можете получить доступ к тегу<Expr::proto_tag
>и<Expr::proto_arity
>, где<Expr::proto_arity
>является интегральной константой MPL.
Нет более простого выражения, чем терминал, и нет более простой операции, чем извлечение его значения. Как мы уже видели, это то, для чего<proto::value()
>.
proto::terminal< std::ostream & >::type cout_ = {std::cout}; // Get the value of the cout_ terminal: std::ostream & sout = proto::value( cout_ ); // Assert that we got back what we put in: assert( &sout == &std::cout );
Для вычисления типа возврата функции<proto::value()
>можно использовать<proto::result_of::value<>
>. Когда параметр<proto::result_of::value<>
>является нессылочным типом, результирующий тип метафункции представляет собой тип значения, подходящего для хранения по значению; то есть от него лишаются эталона верхнего уровня и квалификаторов. Но при инстанцированном с эталонным типом тип результата имеет ссылку, добавленную к нему, что дает тип, подходящий для хранения посредством ссылки. Если вы хотите узнать фактический тип значения терминала, в том числе, хранится ли он по значению или ссылке, вы можете использовать<fusion::result_of::value_at<Expr,0>::type
>.
В нижеследующей таблице резюмируется вышеупомянутый пункт.
Table 29.4. Accessing Value Types
Метафункция вызов |
Если тип стоимости является... |
Результатом является... |
---|---|---|
< | < |
typename boost::remove_const< typename boost::remove_reference<T>::type >::type [a]
|
< | < |
typename boost::add_reference<T>::type
|
< | < |
typename boost::add_reference< typename boost::add_const<T>::type >::type
|
< | < | < |
[a]Если< |
Каждый нетерминальный узел в дереве экспрессии соответствует оператору в выражении, а дети соответствуют операндам или аргументам оператора. Для доступа к ним вы можете использовать шаблон функции<proto::child_c()
>, как показано ниже:
proto::terminal<int>::type i = {42}; // Get the 0-th operand of an addition operation: proto::terminal<int>::type &ri = proto::child_c<0>( i + 2 ); // Assert that we got back what we put in: assert( &i == &ri );
Вы можете использовать метафункцию<proto::result_of::child_c<>
>, чтобы получить тип N-го ребенка экспрессионного узла. Обычно вам все равно, хранится ли ребенок по значению или по ссылке, поэтому, когда вы спрашиваете тип N-го ребенка выражения<Expr
>(где<Expr
>не является эталонным типом), вы получаете тип ребенка после того, как ссылки и cv-квалификаторы были удалены из него.
template<typename Expr> void test_result_of_child_c(Expr const &expr) { typedef typename proto::result_of::child_c<Expr, 0>::type type; // Since Expr is not a reference type, // result_of::child_c<Expr, 0>::type is a // non-cv qualified, non-reference type: BOOST_MPL_ASSERT(( boost::is_same< type, proto::terminal<int>::type > )); } // ... proto::terminal<int>::type i = {42}; test_result_of_child_c( i + 2 );
Однако, если вы запрашиваете тип Nth ребенка<Expr
&
>или<Expr
const&
>(обратите внимание на ссылку), тип результата будет ссылкой, независимо от того, действительно ли ребенок хранится по ссылке или нет. Если вам нужно точно знать, как ребенок хранится в узле, будь то по ссылке или по стоимости, вы можете использовать<fusion::result_of::value_at<Expr,N>::type
>. В следующей таблице описано поведение метафункции<proto::result_of::child_c<>
>.
Table 29.5. Accessing Child Types
Метафункция вызов |
Когда ребенок становится... |
Результатом является... |
---|---|---|
< | < |
typename boost::remove_const< typename boost::remove_reference<T>::type >::type
|
< | < |
typename boost::add_reference<T>::type
|
< | < |
typename boost::add_reference< typename boost::add_const<T>::type >::type
|
< | < | < |
Большинство операторов на C++ являются унарными или двоичными, поэтому доступ к единственному операнду или левому и правому операндам является очень распространенным действием. По этой причине Proto предоставляет функции<proto::child()
>,<proto::left()
>и<proto::right()
>.<proto::child()
>и<proto::left()
>являются синонимами<proto::child_c<0>()
>, а<proto::right()
>является синонимом<proto::child_c<1>()
>.
Есть также<proto::result_of::child<>
>,<proto::result_of::left<>
>и<proto::result_of::right<>
>метафункции, которые просто пересылаются своим<proto::result_of::child_c<>
>аналогам.
Когда вы строите шаблон выражения с помощью Proto, все промежуточные узлы ребенка удерживаютсяпо ссылке. Избегает ненужных копий, что имеет решающее значение, если вы хотите, чтобы ваш EDSL хорошо работал во время выполнения. Естественно, существует опасность, если временные объекты выходят из-под контроля, прежде чем пытаться оценить шаблон выражения. Это особенно проблема в C++0x с новыми<decltype
>и<auto
>ключевыми словами. Подумайте:
// OOPS: "ex" is left holding dangling references auto ex = proto::lit(1) + 2;
Проблема может возникнуть в современном C++, если вы используете<BOOST_TYPEOF()
>или<BOOST_AUTO()
>, или если вы пытаетесь передать шаблон выражения за пределы его составляющих.
В этих случаях вы хотите глубоко скопировать шаблон выражения так, чтобы все промежуточные узлы и терминалы удерживалисьпо значению. Таким образом, вы можете безопасно назначить шаблон выражения для локальной переменной или вернуть его из функции, не беспокоясь о том, что вы будете использовать ссылки. Вы можете сделать это с помощью<proto::deep_copy()
>:
// OK, "ex" has no dangling references auto ex = proto::deep_copy( proto::lit(1) + 2 );
Если вы используетеBoost.Typeof, это будет выглядеть так:
// OK, use BOOST_AUTO() and proto::deep_copy() to // store an expression template in a local variable BOOST_AUTO( ex, proto::deep_copy( proto::lit(1) + 2 ) );
Для работы вышеупомянутого кода необходимо включить заголовок<boost/proto/proto_typeof.hpp
>, который также определяет макрос<
, который автоматически копирует его аргумент. С<BOOST_PROTO_AUTO
>
вышеупомянутый код может быть записан как:BOOST_PROTO_AUTO
>()
// OK, BOOST_PROTO_AUTO() automatically deep-copies // its argument: BOOST_PROTO_AUTO( ex, proto::lit(1) + 2 );
При глубокой копировании дерева выражений все промежуточные узлы и все терминалы хранятся по значению. Единственным исключением являются терминалы, которые являются ссылками на функции.
![]() | Note |
---|---|
< |
Proto предоставляет утилиту для красивой печати деревьев экспрессии, которая очень удобна, когда вы пытаетесь отладить свой EDSL. Он называется<proto::display_expr()
>, и вы передаете ему выражение для печати и необязательно<std::ostream
>для отправки вывода. Подумайте:
// Use display_expr() to pretty-print an expression tree proto::display_expr( proto::lit("hello") + 42 );
Приведенный выше код пишет об этом<std::cout
>:
plus( terminal(hello) , terminal(42) )
Для вызова<proto::display_expr()
>все терминалы в выражении должны быть потоковыми (то есть они могут быть записаны на<std::ostream
>). Кроме того, все типы тегов также должны быть потоковыми. Вот пример, который включает пользовательский тип терминала и пользовательский тег:
// A custom tag type that is Streamable struct MyTag { friend std::ostream &operator<<(std::ostream &s, MyTag) { return s << "MyTag"; } }; // Some other Streamable type struct MyTerminal { friend std::ostream &operator<<(std::ostream &s, MyTerminal) { return s << "MyTerminal"; } }; int main() { // Display an expression tree that contains a custom // tag and a user-defined type in a terminal proto::display_expr( proto::make_expr<MyTag>(MyTerminal()) + 42 ); }
Приведенный выше код печатает следующее:
plus( MyTag( terminal(MyTerminal) ) , terminal(42) )
В следующей таблице перечислены перегружаемые операторы C++, типы тегов Proto для каждого и название метафункций для генерации соответствующих типов экспрессии Proto. Как мы увидим позже, метафункции также можно использовать в качестве грамматики для сопоставления таких узлов, а также сквозных преобразований.
Table 29.6. Operators, Tags and Metafunctions
оператор |
Proto Tag |
Прото-метафункция |
---|---|---|
unary< | < | proto::unary_plus<> |
unary< | < | < |
унарный< | < | < |
unary< | < | < |
unary< | < | < |
unary< | < | < |
унарный пристав< | < | < |
унарный пристав< | < | < |
unary postfix< | < | < |
unary postfix< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
< | < | |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
двоичный< | < | < |
Бинарный подстрочный | < | < |
тройной< | < | < |
вызов функции n-ary | < | < |
Повышаю. Fusion - это библиотека итераторов, алгоритмов, контейнеров и адаптеров для манипулирования гетерогенными последовательностями. По сути, выражение Прото является просто гетерогенной последовательностью его детских выражений, и поэтому выражения Прото являются действительными последовательностями случайного доступа Fusion. Это означает, что вы можете применять к ним алгоритмы Fusion, преобразовывать их, применять к ним фильтры Fusion и просмотры, а также получать доступ к их элементам с помощью<fusion::at()
>. То, что Fusion может сделать с гетерогенными последовательностями, выходит за рамки руководства для пользователей, но ниже приведен простой пример. Он принимает вызов ленивой функции, как<fun(1,2,3,4)
>и использует Fusion для печати аргументов функции в порядке.
struct display { template<typename T> void operator()(T const &t) const { std::cout << t << std::endl; } }; struct fun_t {}; proto::terminal<fun_t>::type const fun = {{}}; // ... fusion::for_each( fusion::transform( // pop_front() removes the "fun" child fusion::pop_front(fun(1,2,3,4)) // Extract the ints from the terminal nodes , proto::functional::value() ) , display() );
Вспомним из Введения, что типы в пространстве имен<proto::functional
>определяют функциональные объекты, которые соответствуют свободным функциям Прото. Так<proto::functional::value()
>создает объект функции, эквивалентный функции<proto::value()
>. Приведенные выше ссылки<fusion::for_each()
>показывают следующее:
1 2 3 4
Терминалы также являются действительными последовательностями Fusion. Они содержат один элемент: их ценность.
Представьте себе небольшую вариацию вышеприведенного примера, где вместо того, чтобы повторяться над аргументами ленивого вызова функции, мы хотели бы повторить над терминалами в дополнительном выражении:
proto::terminal<int>::type const _1 = {1}; // ERROR: this doesn't work! Why? fusion::for_each( fusion::transform( _1 + 2 + 3 + 4 , proto::functional::value() ) , display() );
Причина, по которой это не работает, заключается в том, что выражение<_1
+2+3+
4
>не описывает плоскую последовательность терминалов — оно описывает двоичное дерево. Однако мы можем рассматривать его как плоскую последовательность терминалов, используя функцию Прото<proto::flatten()
>.<proto::flatten()
>возвращает вид, который заставляет дерево выглядеть как плоская последовательность слияния. Если самый верхний узел имеет тип тега<T
>, то элементами сплющенной последовательности являются дочерние узлы, которые имеют тип тега, а не<T
>. Этот процесс оценивается рекурсивно. Таким образом, вышесказанное может быть правильно написано как:
proto::terminal<int>::type const _1 = {1}; // OK, iterate over a flattened view fusion::for_each( fusion::transform( proto::flatten(_1 + 2 + 3 + 4) , proto::functional::value() ) , display() );
Приведенные выше ссылки<fusion::for_each()
>показывают следующее:
1 2 3 4
Дерево экспрессии может иметь очень богатую и сложную структуру. Часто вам нужно знать некоторые вещи о структуре выражения, прежде чем вы сможете его обработать. В этом разделе описаны инструменты, которые Proto предоставляет для заглядывания внутрь дерева выражения и обнаружения его структуры. И, как вы увидите в последующих разделах, все действительно интересные вещи, которые вы можете сделать с Proto, начинаются здесь.
Представьте, что ваш EDSL представляет собой миниатюрный объект ввода-вывода с операциями iostream, которые выполняются лениво. Вы можете захотеть, чтобы выражения, представляющие операции ввода, обрабатывались одной функцией, а операции вывода обрабатывались другой функцией. Как бы ты это сделал?
Ответ заключается в написании шаблонов (а.к.а,грамматики), которые соответствуют структуре входных и выходных выражений. Proto предоставляет утилиты для определения грамматики и шаблон<proto::matches<>
>для проверки соответствия данного типа выражения грамматике.
Во-первых, давайте определим некоторые терминалы, которые мы можем использовать в наших ленивых выражениях ввода/вывода:
proto::terminal< std::istream & >::type cin_ = { std::cin }; proto::terminal< std::ostream & >::type cout_ = { std::cout };
Теперь мы можем использовать<cout_
>вместо<std::cout
>и получить деревья выражения ввода/вывода, которые мы можем выполнить позже. Для определения грамматики, соответствующей входным и выходным выражениям формы<cin_
>>i
>и<cout_<<
1
>, мы делаем следующее:
struct Input : proto::shift_right< proto::terminal< std::istream & >, proto::_ > {}; struct Output : proto::shift_left< proto::terminal< std::ostream & >, proto::_ > {};
Мы видели шаблон<proto::terminal<>
>раньше, но здесь мы используем его без доступа к вложенному<::type
>. При таком использовании это очень простая грамматика, как<proto::shift_right<>
>и<proto::shift_left<>
>. Новичок здесь<_
>в пространстве имен<proto
>. Это дикая карта, которая соответствует всему. Структура<Input
>- это грамматика, которая соответствует любому выражению правого смещения, которое имеет терминал<std::istream
>в качестве левого операнда.
Мы можем использовать эти грамматики вместе с шаблоном<proto::matches<>
>для запроса во время компиляции, является ли данный тип выражения ввода/вывода операцией ввода или вывода. Рассмотрим следующее:
template< typename Expr > void input_output( Expr const & expr ) { if( proto::matches< Expr, Input >::value ) { std::cout << "Input!\n"; } if( proto::matches< Expr, Output >::value ) { std::cout << "Output!\n"; } } int main() { int i = 0; input_output( cout_ << 1 ); input_output( cin_ >> i ); return 0; }
Эта программа печатает следующее:
Output! Input!
Если мы хотим разбить функцию<input_output()
>на две функции, одну, которая обрабатывает входные выражения и одну для выходных выражений, мы можем использовать<boost::enable_if<>
>следующим образом:
template< typename Expr > typename boost::enable_if< proto::matches< Expr, Input > >::type input_output( Expr const & expr ) { std::cout << "Input!\n"; } template< typename Expr > typename boost::enable_if< proto::matches< Expr, Output > >::type input_output( Expr const & expr ) { std::cout << "Output!\n"; }
Это работает так же, как и предыдущая версия. Однако нижеследующее вообще не составляет:
input_output( cout_ << 1 << 2 ); // oops!
Что случилось? Проблема в том, что это выражение не соответствует нашей грамматике. Выражение группы, как если бы оно было написано<(cout_<<1)<<2
>. Он не будет соответствовать грамматике<Output
>, которая ожидает, что левый операнд будет терминалом, а не другой операцией левой смены. Нам нужно исправить грамматику.
Мы замечаем, что для того, чтобы проверить выражение в качестве входа или выхода, нам нужно будет повторить до самого нижнего левого листа и проверить, что это<std::istream
>или<std::ostream
>. Когда мы доберемся до терминала, мы должны перестать повторяться. Мы можем выразить это в нашей грамматике, используя<proto::or_<>
>. Вот правильные<Input
>и<Output
>грамматики:
struct Input : proto::or_< proto::shift_right< proto::terminal< std::istream & >, proto::_ > , proto::shift_right< Input, proto::_ > > {}; struct Output : proto::or_< proto::shift_left< proto::terminal< std::ostream & >, proto::_ > , proto::shift_left< Output, proto::_ > > {};
Сначала это может показаться немного странным. Мы, кажется, определяем типы<Input
>и<Output
>с точки зрения самих себя. Вообще-то, это нормально. В грамматике используются типы<Input
>и<Output
>, онинеполные, но к тому времени, когда мы фактически оценим грамматику с<proto::matches<>
>, типы будут полными. Это рекурсивные грамматики, и это правильно, потому что они должны соответствовать рекурсивной структуре данных.
Соответствие выражения<cout_
<<1
<<2
>грамматике<Output
>происходит следующим образом:
proto::or_<>
>пробуется первой. Оно не сработает, потому что выражение<cout_<<
1<<
2
>не соответствует грамматике<proto::shift_left<
proto::terminal<
std::ostream&
>,proto::_>
>.proto::shift_left<
Output,
proto::_>
>. Выражение - левая смена, поэтому мы попытаемся сопоставить операнды.2
>соответствует<proto::_
>тривиально.cout_
<<1
><Output
>, мы должны рекурсивно оценить грамматику<Output
>. На этот раз мы преуспеем, потому что<cout_
<<1
>будет соответствовать первой альтернативе<proto::or_<>
>.Мы закончили - грамматика успешно совпадает.
Терминалы в дереве экспрессии могут быть ссылками на const или non-const, или они могут вообще не быть ссылками. При написании грамматики вам обычно не нужно беспокоиться об этом, потому что<proto::matches<>
>дает вам немного места для маневра при сопоставлении терминалов. Грамматика, такая как<proto::terminal<int>
>, будет соответствовать терминалу типа<int
>,<int&
>или<intconst
&
>.
Вы можете четко указать, что вы хотите соответствовать типу ссылки. Если да, то тип должен точно совпадать. Например, грамматика, такая как<proto::terminal<int&>
>, будет соответствовать только<int&
>. Он не будет соответствовать<int
>или<int
const&
>.
В таблице ниже показано, как Proto соответствует терминалам. Простое правило: если вы хотите соответствовать только типам ссылок, вы должны указать ссылку в своей грамматике. В противном случае оставьте это, и Proto будет игнорировать конст и ссылки.
Table 29.7. proto::matches<> and Reference / CV-Qualification of Terminals
Терминал |
грамматика |
Спички? |
---|---|---|
Т | Т | Да |
T & | Т | Да |
T const & | Т | Да |
Т | T & | Нет |
T & | T & | Да |
T const & | T & | Нет |
Т | T const & | Нет |
T & | T const & | Нет |
T const & | T const & | Да |
Напрашивается вопрос: Что делать, если вы хотите соответствовать<int
>, но не<int&
>или<intconst
&
>? Для форсирования точных совпадений Proto предоставляет шаблон<proto::exact<>
>. Например,<proto::terminal<proto::exact<int>>
>соответствует<int
>, удерживаемому по стоимости.
Proto дает вам дополнительное пространство для маневра при сопоставлении типов массивов. Типы массивов соответствуют самим себе или типам указателей, на которые они распадаются. Это особенно полезно для массивов символов. Возвращенный<proto::as_expr("hello")
>тип<proto::terminal<charconst[6]>::type
>. Это терминал, содержащий 6-элементный массив символов. Естественно, вы можете сопоставить этот терминал с грамматикой<proto::terminal<charconst[6]>
>, но грамматика<proto::terminal<charconst*>
>также будет соответствовать ему, как иллюстрирует следующий фрагмент кода.
struct CharString : proto::terminal< char const * > {}; typedef proto::terminal< char const[6] >::type char_array; BOOST_MPL_ASSERT(( proto::matches< char_array, CharString > ));
Что, если бы мы только хотели, чтобы<CharString
>соответствовал терминалам именно того типа<char
const*
>? Вы можете использовать<proto::exact<>
>здесь, чтобы отключить нечеткое соответствие терминалов следующим образом:
struct CharString : proto::terminal< proto::exact< char const * > > {}; typedef proto::terminal<char const[6]>::type char_array; typedef proto::terminal<char const *>::type char_string; BOOST_MPL_ASSERT(( proto::matches< char_string, CharString > )); BOOST_MPL_ASSERT_NOT(( proto::matches< char_array, CharString > ));
Теперь<CharString
>не соответствует типам массивов, а только указателям строк символов.
Обратная проблема немного сложнее: что, если вы хотите сопоставить все наборы символов, но не указатели персонажей? Как уже упоминалось выше, выражение<as_expr("hello")
>имеет тип<proto::terminal<charconst[6]>::type
>. Если вы хотите сопоставить массивы символов произвольного размера, вы можете использовать<proto::N
>, который представляет собой дикую карту размером с массив. Следующая грамматика будет соответствовать любой строке буквально:<proto::terminal<charconst[proto::N]>
>.
Иногда вам нужно еще больше места для маневра при сопоставлении терминалов. Например, возможно, вы строите калькулятор EDSL и хотите разрешить любые терминалы, которые можно конвертировать в<double
>. Для этого Proto предоставляет шаблон<proto::convertible_to<>
>. Вы можете использовать его как:<proto::terminal<proto::convertible_to<double
>>
>.
Есть еще один способ выполнить нечеткий матч на терминалах. Рассмотрим проблему сопоставления терминала<std::complex<>
>. Вы можете легко сопоставить<std::complex<float>
>или<std::complex<double>
>, но как бы вы сопоставили любую инстанцию<std::complex<>
>? Вы можете использовать<proto::_
>здесь, чтобы решить эту проблему. Вот грамматика, чтобы соответствовать любой<std::complex<>
>инстанциации:
struct StdComplex : proto::terminal< std::complex< proto::_ > > {};
Когда дается такая грамматика, Прото деконструирует грамматику и терминал, с которым она сопоставляется, и посмотрит, может ли она соответствовать всем составляющим.
Мы уже видели, как использовать генераторы выражений<proto::terminal<>
>и<proto::shift_right<>
>в качестве грамматики. Мы также видели<proto::or_<>
>, который мы можем использовать для выражения набора альтернативных грамматик. Есть несколько других, представляющих интерес; в частности,<proto::if_<>
>,<proto::and_<>
>и<proto::not_<>
>.
Шаблон<proto::not_<>
>самый простой. Он принимает грамматику как параметр шаблона и логически отрицает ее;<not_<Grammar>
>будет соответствовать любому выражению, которое<Grammar
>делает, а не.
Шаблон<proto::if_<>
>используется вместе с преобразованием Прото, которое оценивается по типам экспрессии для поиска совпадений. (Прототрансформация будет описана позже.)
Шаблон<proto::and_<>
>подобен<proto::or_<>
>, за исключением того, что каждый аргумент<proto::and_<>
>должен соответствовать<proto::and_<>
>. В качестве примера рассмотрим определение<CharString
>выше, которое использует<proto::exact<>
>. Он мог быть написан без<proto::exact<>
>следующим образом:
struct CharString : proto::and_< proto::terminal< proto::_ > , proto::if_< boost::is_same< proto::_value, char const * >() > > {};
Это говорит о том, что<CharString
>должен быть терминалом,иего тип значения должен быть таким же, как<charconst
*
>. Обратите внимание на шаблонный аргумент<proto::if_<>
>:<boost::is_same<proto::_value,charconst*>()
>. Это преобразование Прото, которое сравнивает тип значения терминала с<char
const*
>.
Шаблон<proto::if_<>
>имеет несколько вариантов. В дополнение к<if_<Condition>
>можно также сказать<if_<Condition,ThenGrammar>
>и<if_<Condition,ThenGrammar,ElseGrammar>
>. Они позволяют выбрать одну подграмму или другую на основе<Condition
>.
Когда ваша грамматика Proto станет большой, вы начнете сталкиваться с некоторыми проблемами масштабируемости с<proto::or_<>
>, конструкцией, которую вы используете для указания альтернативных подграмм. Во-первых, из-за ограничений в C++<proto::or_<>
>может принимать только определенное количество подграмм, контролируемых макросом<BOOST_PROTO_MAX_LOGICAL_ARITY
>. Этот макрос по умолчанию равен восьми, и вы можете установить его выше, но это усугубит еще одну проблему масштабируемости: длительное время компиляции. С<proto::or_<>
>альтернативные подграммы проверяются в порядке, как серия каскадных<if
>, что приводит к множеству ненужных шаблонных инстанциаций. Вместо этого вы предпочли бы что-то вроде<switch
>, что позволяет избежать затрат на каскадирование<if
>'s. Это цель<proto::switch_<>
>; хотя это и менее удобно, чем<proto::or_<>
>, это улучшает время компиляции для более крупных грамматик и не имеет произвольного фиксированного ограничения на количество подграмм.
Давайте проиллюстрируем, как использовать<proto::switch_<>
>, сначала написав большую грамматику<proto::or_<>
>, а затем переведя ее на эквивалентную грамматику, используя<proto::switch_<>
>:
// Here is a big, inefficient grammar struct ABigGrammar : proto::or_< proto::terminal<int> , proto::terminal<double> , proto::unary_plus<ABigGrammar> , proto::negate<ABigGrammar> , proto::complement<ABigGrammar> , proto::plus<ABigGrammar, ABigGrammar> , proto::minus<ABigGrammar, ABigGrammar> , proto::or_< proto::multiplies<ABigGrammar, ABigGrammar> , proto::divides<ABigGrammar, ABigGrammar> , proto::modulus<ABigGrammar, ABigGrammar> > > {};
Выше может быть грамматика для более сложного калькулятора EDSL. Заметьте, что, поскольку существует более восьми подграмм, мы должны были связать подграммы с вложенным<proto::or_<>
>.
Идея, лежащая в основе<proto::switch_<>
>, заключается в отправке на основе типа тега выражения в подграмму, которая обрабатывает выражения этого типа. Для использования<proto::switch_<>
>вы определяете структуру с вложенным<case_<>
>шаблоном, специализирующимся на типах тегов. Вышеприведенная грамматика может быть выражена следующим образом<proto::switch_<>
>. Это описано ниже.
// Redefine ABigGrammar more efficiently using proto::switch_<> struct ABigGrammar; struct ABigGrammarCases { // The primary template matches nothing: template<typename Tag> struct case_ : proto::not_<_> {}; }; // Terminal expressions are handled here template<> struct ABigGrammarCases::case_<proto::tag::terminal> : proto::or_< proto::terminal<int> , proto::terminal<double> > {}; // Non-terminals are handled similarly template<> struct ABigGrammarCases::case_<proto::tag::unary_plus> : proto::unary_plus<ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::negate> : proto::negate<ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::complement> : proto::complement<ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::plus> : proto::plus<ABigGrammar, ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::minus> : proto::minus<ABigGrammar, ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::multiplies> : proto::multiplies<ABigGrammar, ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::divides> : proto::divides<ABigGrammar, ABigGrammar> {}; template<> struct ABigGrammarCases::case_<proto::tag::modulus> : proto::modulus<ABigGrammar, ABigGrammar> {}; // Define ABigGrammar in terms of ABigGrammarCases // using proto::switch_<> struct ABigGrammar : proto::switch_<ABigGrammarCases> {};
Сопоставление типа выражения<E
>с<proto::switch_<C>
>эквивалентно сопоставлению его с<C::case_<E::proto_tag>
>. Отправляя по типу тега выражения, мы можем перейти к подграмме, которая обрабатывает выражения этого типа, пропуская все другие подграммы, которые не могут соответствовать. Если нет специализации<case_<>
>для конкретного типа тега, мы выбираем основной шаблон. В этом случае основной шаблон наследуется от<proto::not_<_>
>, который не соответствует выражениям.
Обратите внимание на специализацию, которая обрабатывает терминалы:
// Terminal expressions are handled here template<> struct ABigGrammarCases::case_<proto::tag::terminal> : proto::or_< proto::terminal<int> , proto::terminal<double> > {};
Тип<proto::tag::terminal
>сам по себе не достаточно, чтобы выбрать соответствующую подграмму, поэтому мы используем<proto::or_<>
>, чтобы перечислить альтернативные подграммы, которые соответствуют терминалам.
![]() | Note |
---|---|
У вас может возникнуть соблазн определить свои специализации< struct ABigGrammarCases { template<typename Tag> struct case_ : proto::not_<_> {}; // ERROR: not legal C++ template<> struct case_<proto::tag::terminal> /* ... */ }; К сожалению, по тайным причинам незаконным является определение явной вложенной специализацииin situ. Однако совершенно законно определятьчастичныеспециализацииin situ, поэтому вы можете добавить дополнительный параметр шаблона шаблона, который имеет по умолчанию, следующим образом: struct ABigGrammarCases { // Note extra "Dummy" template parameter here: template<typename Tag, int Dummy = 0> struct case_ : proto::not_<_> {}; // OK: "Dummy" makes this a partial specialization // instead of an explicit specialization. template<int Dummy> struct case_<proto::tag::terminal, Dummy> /* ... */ }; Вы можете найти это более чистым, чем определение явных специализаций< |
Не все перегружаемые операторы C++ являются унарными или бинарными. Существует нечетное число<operator()
>— оператор вызова функции — которое может иметь любое количество аргументов. Аналогичным образом, с Proto вы можете определить своих собственных «операторов», которые также могут принять более двух аргументов. В результате в дереве экспрессии Прото могут быть узлы, имеющие произвольное число детей (до<
, которое настраивается). Как написать грамматику, чтобы соответствовать такому узлу?BOOST_PROTO_MAX_ARITY
>
Для таких случаев Proto предоставляет шаблон класса<proto::vararg<>
>. Его аргумент шаблона является грамматикой, и 1408 будет соответствовать грамматике ноль или более раз. Рассмотрим ленивую функцию Proto, называемую<fun()
>, которая может принимать в качестве аргументов ноль или более символов:
struct fun_tag {}; struct FunTag : proto::terminal< fun_tag > {}; FunTag::type const fun = {{}}; // example usage: fun(); fun('a'); fun('a', 'b'); ...
Ниже приведена грамматика, которая соответствует всем допустимым призывам<fun()
>:
struct FunCall : proto::function< FunTag, proto::vararg< proto::terminal< char > > > {};
Грамматика<FunCall
>использует<proto::vararg<>
>для соответствия нулю или более буквальным символам в качестве аргументов функции<fun()
>.
В качестве другого примера, можете ли вы угадать, что соответствует следующей грамматике?
struct Foo : proto::or_< proto::terminal< proto::_ > , proto::nary_expr< proto::_, proto::vararg< Foo > > > {};
Первый параметр шаблона<proto::nary_expr<>
>представляет тип узла, а любые дополнительные параметры шаблона представляют дочерние узлы. Ответ заключается в том, что это дегенеративная грамматика, которая соответствует каждому возможному дереву выражения, от корня до листьев.
В этом разделе мы рассмотрим, как использовать Proto для определения грамматики для вашего EDSL и использовать его для проверки шаблонов выражения, давая короткие, читаемые ошибки времени компиляции для недействительных выражений.
![]() | Tip |
---|---|
Вы можете подумать, что это обратный способ делать вещи.& #8220;Если бы Proto позволил мне выбрать, какие операторы перегружать, мои пользователи не смогли бы создать недействительные выражения в первую очередь, и мне бы вообще не нужна была грамматика!& #8221;Это может быть правдой, но есть причины, по которым люди предпочитают делать что-то именно так. Во-первых, это позволяет вам быстро развивать свой EDSL - все операторы уже есть для вас! - и беспокоиться о недействительном синтаксисе позже. Во-вторых, некоторые операторы могут быть разрешены только в определенных контекстах. Это легко выразить с помощью грамматики, и трудно сделать с прямой перегрузкой оператора. В-третьих, использование грамматики EDSL для обозначения недействительных выражений часто приводит к лучшим ошибкам, чем ручной выбор перегруженных операторов. В-четвертых, грамматика может использоваться не только для проверки. Вы можете использовать свою грамматику для определенияпреобразований деревьев, которые преобразуют шаблоны выражения в другие более полезные объекты. Если ничто из вышеперечисленного не убеждает вас, вы на самом делеможетеиспользовать Proto для контроля того, какие операторы перегружены в вашем домене. И для этого нужно определить грамматику! |
В предыдущем разделе мы использовали Proto для определения EDSL для лениво оцененного калькулятора, который позволял любую комбинацию заполнителей, буквы с плавающей точкой, сложение, вычитание, умножение, деление и группирование. Если бы мы написали грамматику для этого EDSL вEBNF, это могло бы выглядеть так:
group ::= '(' expression ')' factor ::= double | '_1' | '_2' | group term ::= factor (('*' factor) | ('/' factor))* expression ::= term (('+' term) | ('-' term))*
Это фиксирует синтаксис, ассоциативность и правила приоритета калькулятора. Написание грамматики для нашего калькулятора EDSL с помощью Protoеще проще. Поскольку мы используем C++ в качестве основного языка, мы привязаны к правилам ассоциативности и приоритетности для операторов C++. Наша грамматика может их принять. Кроме того, в C++ группирование уже обрабатывается для нас с использованием скобки, поэтому нам не нужно кодировать это в нашей грамматике.
Давайте начнем нашу грамматику, чтобы объявить ее вперед:
struct CalculatorGrammar;
На данный момент это неполный тип, но мы все равно сможем использовать его для определения правил нашей грамматики. Определим правила грамматики для терминалов:
struct Double : proto::terminal< proto::convertible_to< double > > {}; struct Placeholder1 : proto::terminal< placeholder<0> > {}; struct Placeholder2 : proto::terminal< placeholder<1> > {}; struct Terminal : proto::or_< Double, Placeholder1, Placeholder2 > {};
Теперь определим правила сложения, вычитания, умножения и деления. Здесь мы можем игнорировать вопросы ассоциативности и приоритетности — компилятор C++ будет обеспечивать это для нас. Мы должны лишь настаивать на том, чтобы доводы операторов сами соответствовали тому<CalculatorGrammar
>, о котором мы заявляли выше.
struct Plus : proto::plus< CalculatorGrammar, CalculatorGrammar > {}; struct Minus : proto::minus< CalculatorGrammar, CalculatorGrammar > {}; struct Multiplies : proto::multiplies< CalculatorGrammar, CalculatorGrammar > {}; struct Divides : proto::divides< CalculatorGrammar, CalculatorGrammar > {};
Теперь, когда мы определили все части грамматики, мы можем определить<CalculatorGrammar
>:
struct CalculatorGrammar : proto::or_< Terminal , Plus , Minus , Multiplies , Divides > {};
Вот так! Теперь мы можем использовать<CalculatorGrammar
>для обеспечения того, чтобы шаблон выражения соответствовал нашей грамматике. Мы можем использовать<proto::matches<>
>и<BOOST_MPL_ASSERT()
>для выпуска читаемых ошибок компиляции для недействительных выражений, как показано ниже:
template< typename Expr > void evaluate( Expr const & expr ) { BOOST_MPL_ASSERT(( proto::matches< Expr, CalculatorGrammar > )); // ... }
Теперь, когда вы написали переднюю часть для компилятора EDSL и немного узнали о промежуточной форме, которую он производит, пришло время подумать о том, что делатьс промежуточной формой. Здесь вы размещаете свои алгоритмы и оптимизации для домена. Proto дает вам два способа оценки и управления шаблонами выражения: контексты и преобразования.
proto::eval()
>. Он связывает поведение с типами узлов.<proto::eval()
>ходит по выражению и вызывает ваш контекст в каждом узле.Два способа оценки выражений! Как выбрать? Поскольку контексты в основном процедурные, их немного проще понять и отладить, поэтому они являются хорошим местом для начала. Но хотя преобразования более продвинуты, они также более мощны; поскольку они связаны с правилами в вашей грамматике, вы можете выбрать правильное преобразование на основе всей структурыподвыражения, а не просто по типу его верхнего узла.
Кроме того, преобразования имеют лаконичный и декларативный синтаксис, который может сначала сбивать с толку, но очень выразительный и взаимозаменяемый, как только вы привыкнете к нему. И — это, по общему признанию, очень субъективно — автор находит программирование с помощью трансформаций Прото непомерным количествомудовольствия!Ваш пробег может варьироваться.
После того, как вы построили дерево экспрессии Прото, либо с помощью перегрузок оператора Прото, либо с помощью<proto::make_expr()
>и друзей, вы, вероятно, захотите на самом делесделатьчто-то с ним. Самый простой вариант — использовать<proto::eval()
>, общий оценщик выражений. Чтобы использовать<proto::eval()
>, вам нужно определитьконтекст, который сообщает<proto::eval()
>, как каждый узел должен быть оценен. В этом разделе рассматриваются гайки и болты использования<proto::eval()
>, определение контекстов оценки и использование контекстов, которые предоставляет Proto.
![]() | Note |
---|---|
< |
Синопсис:
namespace proto { namespace result_of { // A metafunction for calculating the return // type of proto::eval() given certain Expr // and Context types. template<typename Expr, typename Context> struct eval { typedef typename Context::template eval<Expr>::result_type type; }; } namespace functional { // A callable function object type for evaluating // a Proto expression with a certain context. struct eval : callable { template<typename Sig> struct result; template<typename Expr, typename Context> typename proto::result_of::eval<Expr, Context>::type operator ()(Expr &expr, Context &context) const; template<typename Expr, typename Context> typename proto::result_of::eval<Expr, Context>::type operator ()(Expr &expr, Context const &context) const; }; } template<typename Expr, typename Context> typename proto::result_of::eval<Expr, Context>::type eval(Expr &expr, Context &context); template<typename Expr, typename Context> typename proto::result_of::eval<Expr, Context>::type eval(Expr &expr, Context const &context); }
Учитывая выражение и контекст оценки, использование<proto::eval()
>довольно просто. Просто передайте выражение и контекст<proto::eval()
>, и он сделает все остальное и вернет результат. Вы можете использовать метафункцию<eval<>
>в пространстве имен<proto::result_of
>для вычисления типа возврата<proto::eval()
>. Ниже показано использование<proto::eval()
>:
template<typename Expr> typename proto::result_of::eval<Expr const, MyContext>::type MyEvaluate(Expr const &expr) { // Some user-defined context type MyContext ctx; // Evaluate an expression with the context return proto::eval(expr, ctx); }
То, что делает<proto::eval()
>, также очень просто. Он относит большую часть работы к самому контексту. Вот, по существу, реализация<proto::eval()
>:
// eval() dispatches to a nested "eval<>" function // object within the Context: template<typename Expr, typename Context> typename Context::template eval<Expr>::result_type eval(Expr &expr, Context &ctx) { typename Context::template eval<Expr> eval_fun; return eval_fun(expr, ctx); }
Действительно,<proto::eval()
>— это не что иное, как тонкая обертка, которая посылает соответствующему обработчику в рамках контекстного класса. В следующем разделе мы увидим, как реализовать класс контекста с нуля.
Как мы видели в предыдущем разделе, в функции<proto::eval()
>действительно не так много. Скорее, вся интересная оценка выражения проходит в рамках контекстного класса. В этом разделе показано, как реализовать один с нуля.
Все классы контекста имеют примерно следующую форму:
// A prototypical user-defined context. struct MyContext { // A nested eval<> class template template< typename Expr , typename Tag = typename proto::tag_of<Expr>::type > struct eval; // Handle terminal nodes here... template<typename Expr> struct eval<Expr, proto::tag::terminal> { // Must have a nested result_type typedef. typedef ... result_type; // Must have a function call operator that takes // an expression and the context. result_type operator()(Expr &expr, MyContext &ctx) const { return ...; } }; // ... other specializations of struct eval<> ... };
Контекстные классы — это не что иное, как набор специализаций вложенного шаблона классов<eval<>
>. Каждая специализация имеет различный тип выражения.
В разделеHello Calculatorмы увидели пример определяемого пользователем класса контекста для оценки выражений калькулятора. Этот класс контекста был реализован с помощью Прото<proto::callable_context<>
>. Если бы мы реализовали его с нуля, это выглядело бы примерно так:
// The calculator_context from the "Hello Calculator" section, // implemented from scratch. struct calculator_context { // The values with which we'll replace the placeholders std::vector<double> args; template< typename Expr // defaulted template parameters, so we can // specialize on the expressions that need // special handling. , typename Tag = typename proto::tag_of<Expr>::type , typename Arg0 = typename proto::child_c<Expr, 0>::type > struct eval; // Handle placeholder terminals here... template<typename Expr, int I> struct eval<Expr, proto::tag::terminal, placeholder<I> > { typedef double result_type; result_type operator()(Expr &, MyContext &ctx) const { return ctx.args[I]; } }; // Handle other terminals here... template<typename Expr, typename Arg0> struct eval<Expr, proto::tag::terminal, Arg0> { typedef double result_type; result_type operator()(Expr &expr, MyContext &) const { return proto::child(expr); } }; // Handle addition here... template<typename Expr, typename Arg0> struct eval<Expr, proto::tag::plus, Arg0> { typedef double result_type; result_type operator()(Expr &expr, MyContext &ctx) const { return proto::eval(proto::left(expr), ctx) + proto::eval(proto::right(expr), ctx); } }; // ... other eval<> specializations for other node types ... };
Теперь мы можем использовать<proto::eval()
>с классом контекста выше для оценки выражений калькулятора следующим образом:
// Evaluate an expression with a calculator_context calculator_context ctx; ctx.args.push_back(5); ctx.args.push_back(6); double d = proto::eval(_1 + _2, ctx); assert(11 == d);
Определение контекста с нуля таким образом утомительно и многословно, но это дает вам полный контроль над тем, как оценивается выражение. Класс контекста в примереHello Calculatorбыл намного проще. В следующем разделе мы увидим класс помощников Proto, чтобы облегчить работу по внедрению контекстных классов.
Proto предоставляет некоторые готовые классы контекста, которые вы можете использовать как есть, или которые вы можете использовать, чтобы помочь при реализации ваших собственных контекстов. Они:
default_context
Контекст оценки, который присваивает обычные значения C++ всем операторам. Например, дополнительные узлы обрабатываются путем оценки левых и правых детей, а затем добавляют результаты. В 1498 г. используется буст. Тип для определения типов выражений, которые он оценивает.
null_context
Простой контекст, который рекурсивно оценивает детей, но никак не комбинирует результаты и возвращает пустоту.
callable_context<>
Помощник, который упрощает работу по написанию контекстных классов. Вместо того, чтобы писать шаблонные специализации, с<proto::callable_context<>
>вы пишете объект функции с перегруженным оператором вызова функции. Любые выражения, не обработанные перегрузкой, автоматически отправляются в контекст оценки по умолчанию, который вы можете указать.
<proto::default_context
>— это контекст оценки, который присваивает всем операторам обычные значения C++. Например, дополнительные узлы обрабатываются путем оценки левых и правых детей, а затем добавляют результаты.<proto::default_context
>использует Boost. Тип для определения типов выражений, которые он оценивает.
Рассмотрим, например, следующий пример «Hello World»:
#include <iostream> #include <boost/proto/proto.hpp> #include <boost/proto/context.hpp> #include <boost/typeof/std/ostream.hpp> using namespace boost; proto::terminal< std::ostream & >::type cout_ = { std::cout }; template< typename Expr > void evaluate( Expr const & expr ) { // Evaluate the expression with default_context, // to give the operators their C++ meanings: proto::default_context ctx; proto::eval(expr, ctx); } int main() { evaluate( cout_ << "hello" << ',' << " world" ); return 0; }
Эта программа предусматривает следующее:
hello, world
<proto::default_context
>тривиально определяется в терминах шаблона<default_eval<>
>следующим образом:
// Definition of default_context struct default_context { template<typename Expr> struct eval : default_eval< Expr , default_context const , typename tag_of<Expr>::type > {}; };
Существует множество специализаций<default_eval<>
>, каждая из которых обрабатывает другого оператора C++. Вот, например, специализация для бинарного сложения:
// A default expression evaluator for binary addition template<typename Expr, typename Context> struct default_eval<Expr, Context, proto::tag::plus> { private: static Expr & s_expr; static Context & s_ctx; public: typedef decltype( proto::eval(proto::child_c<0>(s_expr), s_ctx) + proto::eval(proto::child_c<1>(s_expr), s_ctx) ) result_type; result_type operator ()(Expr &expr, Context &ctx) const { return proto::eval(proto::child_c<0>(expr), ctx) + proto::eval(proto::child_c<1>(expr), ctx); } };
Приведенный выше код использует<decltype
>для вычисления типа возврата оператора вызова функции.<decltype
>— новое ключевое слово в следующей версии C++, которое получает тип любого выражения. Большинство компиляторов пока не поддерживают<decltype
>напрямую, поэтому<default_eval<>
>использует Boost. Тип библиотеки для подражания. На некоторых компиляторах это может означать, что<default_context
>либо не работает, либо требует, чтобы вы регистрировали свои типы с помощью Boost. Тип библиотеки. Проверьте документацию для Boost. Тип, чтобы увидеть.
1510 — это простой контекст, который рекурсивно оценивает детей, но никоим образом не комбинирует результаты и возвращает пустоту. Это полезно в сочетании с<callable_context<>
>или при определении ваших собственных контекстов, которые мутируют дерево выражения на месте, а не накапливают результат, как мы увидим ниже.
<proto::null_context<>
>тривиально реализуется в терминах<null_eval<>
>следующим образом:
// Definition of null_context struct null_context { template<typename Expr> struct eval : null_eval<Expr, null_context const, Expr::proto_arity::value> {}; };
И<null_eval<>
>также тривиально реализован. Вот, например, двоичный<null_eval<>
>:
// Binary null_eval<> template<typename Expr, typename Context> struct null_eval<Expr, Context, 2> { typedef void result_type; void operator()(Expr &expr, Context &ctx) const { proto::eval(proto::child_c<0>(expr), ctx); proto::eval(proto::child_c<1>(expr), ctx); } };
Когда такие классы будут полезны? Представьте, что у вас есть дерево выражений с целыми терминалами, и вы хотите увеличить каждое целое число на месте. Вы можете определить контекст оценки следующим образом:
struct increment_ints { // By default, just evaluate all children by delegating // to the null_eval<> template<typename Expr, typename Arg = proto::result_of::child<Expr>::type> struct eval : null_eval<Expr, increment_ints const> {}; // Increment integer terminals template<typename Expr> struct eval<Expr, int> { typedef void result_type; void operator()(Expr &expr, increment_ints const &) const { ++proto::child(expr); } }; };
В следующем разделе<proto::callable_context<>
>мы увидим еще более простой способ добиться того же.
<proto::callable_context<>
>— помощник, упрощающий работу по написанию контекстных классов. Вместо написания шаблонных специализаций с 1517 вы пишете объект функции с перегруженным оператором вызова функции. Любые выражения, не обработанные перегрузкой, автоматически отправляются в контекст оценки по умолчанию, который вы можете указать.
Вместо контекста оценки в его собственном праве<proto::callable_context<>
>более правильно мыслится как адаптер контекста. Чтобы использовать его, вы должны определить свой собственный контекст, который наследуется от<proto::callable_context<>
>.
В разделе<null_context
>мы увидели, как реализовать контекст оценки, который увеличивает все целые числа в дереве экспрессии. Вот как сделать то же самое с<proto::callable_context<>
>:
// An evaluation context that increments all // integer terminals in-place. struct increment_ints : callable_context< increment_ints const // derived context , null_context const // fall-back context > { typedef void result_type; // Handle int terminals here: void operator()(proto::tag::terminal, int &i) const { ++i; } };
С таким контекстом мы можем сделать следующее:
literal<int> i = 0, j = 10; proto::eval( i - j * 3.14, increment_ints() ); std::cout << "i = " << i.get() << std::endl; std::cout << "j = " << j.get() << std::endl;
Эта программа выводит следующее, которое показывает, что целые числа<i
>и<j
>были увеличены<1
>:
i = 1 j = 11
В контексте<increment_ints
>нам не нужно было определять какие-либо вложенные шаблоны<eval<>
>. Это потому, что<proto::callable_context<>
>реализует их для нас.<proto::callable_context<>
>берет два параметра шаблона: производный контекст и обратный контекст. Для каждого узла в оцениваемом дереве экспрессии<proto::callable_context<>
>проверяет, есть ли перегруженный<operator()
>в производном контексте, который принимает его. Учитывая некоторое выражение<expr
>типа<Expr
>и контекст<ctx
>, он пытается назвать:
ctx( typename Expr::proto_tag() , proto::child_c<0>(expr) , proto::child_c<1>(expr) ... );
Используя трюки перегрузки функций и метапрограммирования,<proto::callable_context<>
>может определить, существует ли такая функция или нет. Если да, то эта функция называется. Если нет, то текущее выражение передается в контекст обратной оценки для обработки.
Мы видели еще один пример<proto::callable_context<>
>, когда смотрели на простой калькулятор выражения оценщик. Там мы хотели настроить оценку терминалов-заполнителей и делегировать обработку всех других узлов<proto::default_context
>. Мы сделали это следующим образом:
// An evaluation context for calculator expressions that // explicitly handles placeholder terminals, but defers the // processing of all other nodes to the default_context. struct calculator_context : proto::callable_context< calculator_context const > { std::vector<double> args; // Define the result type of the calculator. typedef double result_type; // Handle the placeholders: template<int I> double operator()(proto::tag::terminal, placeholder<I>) const { return this->args[I]; } };
В этом случае мы не указали обратный контекст. В этом случае<proto::callable_context<>
>использует<proto::default_context
>. С приведенными выше<calculator_context
>и несколькими соответствующим образом определенными терминалами заполнителя мы можем оценить выражения калькулятора, как показано ниже:
template<int I> struct placeholder {}; terminal<placeholder<0> >::type const _1 = {{}}; terminal<placeholder<1> >::type const _2 = {{}}; // ... calculator_context ctx; ctx.args.push_back(4); ctx.args.push_back(5); double j = proto::eval( (_2 - _1) / _2 * 100, ctx ); std::cout << "j = " << j << std::endl;
Приведенный выше код отображает следующее:
j = 20
Если вы когда-либо строили парсер с помощью такого инструмента, как Antlr, yacc или Boost. Дух, вы можете быть знакомы ссемантических действий. В дополнение к определению грамматики языка, распознаваемого парсером, эти инструменты позволяют встраивать код в грамматику, которая выполняется, когда части грамматики участвуют в разборе. Прото имеет эквивалент семантических действий. Они называютсяпреобразованиями. В этом разделе описывается, как встраивать преобразования в грамматику Proto, превращая грамматику в функциональные объекты, которые могут манипулировать или оценивать выражения мощными способами.
Преобразования прото являются продвинутой темой. Мы будем делать это медленно, используя примеры для иллюстрации ключевых понятий, начиная с простого.
Грамматика Прото, которую мы видели до сих пор, статична. Вы можете проверить, соответствует ли тип выражения грамматике, но это все. Вещи становятся более интересными, когда вы даете им поведение во время выполнения. Грамматика со встроенными преобразованиями — это больше, чем просто статическая грамматика. Это функциональный объект, который принимает выражения, соответствующие грамматике и делает.что-тос ними.
Ниже приведена очень простая грамматика. Он соответствует терминальным выражениям.
// A simple Proto grammar that matches all terminals proto::terminal< _ >
Вот та же грамматика с преобразованием, которое извлекает значение из терминала:
// A simple Proto grammar that matches all terminals // *and* a function object that extracts the value from // the terminal proto::when< proto::terminal< _ > , proto::_value // <-- Look, a transform! >
Вы можете прочитать это следующим образом: когда вы соответствуете терминальному выражению, извлеките значение. Тип<proto::_value
>— так называемое преобразование. Позже мы увидим, что делает его трансформацией, но пока просто представим его как своего рода функциональный объект. Обратите внимание на использование<proto::when<>
>: Первый параметр шаблона - это грамматика для соответствия, а второй - преобразование для выполнения. Результатом является как грамматика, которая соответствует терминальным выражениям, так и объект функции, который принимает терминальные выражения и извлекает их значения.
Как и в обычных грамматиках, мы можем определить пустую структуру, которая наследуется от грамматики + преобразования, чтобы дать нам простой способ вернуться к тому, что мы определяем, следующим образом:
// A grammar and a function object, as before struct Value : proto::when< proto::terminal< _ > , proto::_value > {}; // "Value" is a grammar that matches terminal expressions BOOST_MPL_ASSERT(( proto::matches< proto::terminal<int>::type, Value > )); // "Value" also defines a function object that accepts terminals // and extracts their value. proto::terminal<int>::type answer = {42}; Value get_value; int i = get_value( answer );
Как уже упоминалось,<Value
>является грамматикой, которая соответствует терминальным выражениям и функциональному объекту, который работает на терминальных выражениях. Было бы ошибкой передавать нетерминальное выражение объекту функции<Value
>. Это общее свойство грамматики с преобразованиями; при использовании их в качестве функциональных объектов передаваемые им выражения должны соответствовать грамматике.
Протограмматики являются действительными объектами функций в стиле TR1. Это означает, что вы можете использовать<boost::result_of<>
>, чтобы спросить грамматику, каким будет ее тип возврата, учитывая конкретный тип выражения. Например, мы можем получить доступ к типу возврата грамматики<Value
>следующим образом:
// We can use boost::result_of<> to get the return type // of a Proto grammar. typedef typename boost::result_of<Value(proto::terminal<int>::type)>::type result_type; // Check that we got the type we expected BOOST_MPL_ASSERT(( boost::is_same<result_type, int> ));
![]() | Note |
---|---|
Грамматика со встроенными преобразованиями является одновременно грамматикой и функциональным объектом. Называть эти вещи «грамматиками с преобразованиями» было бы утомительно. Мы могли бы назвать их чем-то вроде «активных грамматик», но, как мы увидим, каждаяграмматика, которую вы можете определить с помощью Прото, является «активной»; то есть каждая грамматика имеет некоторое поведение при использовании в качестве функционального объекта. Поэтому мы будем продолжать называть эти вещи простыми «грамматиками». Термин «трансформация» зарезервирован для того, что используется в качестве второго параметра шаблона< |
Большинство грамматик немного сложнее, чем в предыдущем разделе. Для иллюстрации давайте определим довольно бессмысленную грамматику, которая соответствует любому выражению и повторяется до самого левого конца и возвращает его значение. Он продемонстрирует, как две ключевые концепции грамматики Прото — чередование и рекурсия — взаимодействуют с преобразованиями. Грамматика описана ниже.
// A grammar that matches any expression, and a function object // that returns the value of the leftmost terminal. struct LeftmostLeaf : proto::or_< // If the expression is a terminal, return its value proto::when< proto::terminal< _ > , proto::_value > // Otherwise, it is a non-terminal. Return the result // of invoking LeftmostLeaf on the 0th (leftmost) child. , proto::when< _ , LeftmostLeaf( proto::_child0 ) > > {}; // A Proto terminal wrapping std::cout proto::terminal< std::ostream & >::type cout_ = { std::cout }; // Create an expression and use LeftmostLeaf to extract the // value of the leftmost terminal, which will be std::cout. std::ostream & sout = LeftmostLeaf()( cout_ << "the answer: " << 42 << '\n' );
Мы уже видели<proto::or_<>
>. Здесь он выполняет две роли. Во-первых, это грамматика, которая соответствует любой из его альтернативных подграмм; в этом случае, либо терминал или нетерминал. Во-вторых, это также функциональный объект, который принимает выражение, находит альтернативную подграмму, которая соответствует выражению, и применяет его преобразование. А поскольку<LeftmostLeaf
>наследует от<proto::or_<>
>,<LeftmostLeaf
>является одновременно и грамматикой, и функциональным объектом.
![]() | Note |
---|---|
Второй вариант использует< |
Следующий раздел описывает эту грамматику далее.
В грамматике, определенной в предыдущем разделе, преобразование, связанное с нетерминалами, выглядит немного странно:
proto::when< _ , LeftmostLeaf( proto::_child0 ) // <-- a "callable" transform >
Он имеет эффект принятия не терминальных выражений, взятия 0-го (самого левого) ребенка и рекурсивного вызова на него функции<LeftmostLeaf
>. Но<LeftmostLeaf(proto::_child0
)
>на самом деле является.тип функции. Буквально это тип функции, которая принимает объект типа<proto::_child0
>и возвращает объект типа<LeftmostLeaf
>. Итак, как мы понимаем смысл этой трансформации? Очевидно, что нет никакой функции, которая действительно имеет эту подпись, и такая функция не была бы полезной. Ключ в понимании того, как<proto::when<>
>интерпретируетего второй параметр шаблона.
Когда второй параметр шаблона<proto::when<>
>является типом функции,<proto::when<>
>интерпретирует тип функции как преобразование. В этом случае<LeftmostLeaf
>рассматривается как тип объекта функции, на который следует ссылаться, а<proto::_child0
>рассматривается как преобразование. Во-первых,<proto::_child0
>применяется к текущему выражению (нетерминал, который соответствовал этой альтернативной подграмме), и результат (0-й ребенок) передается в качестве аргумента<LeftmostLeaf
>.
![]() | Note |
---|---|
Преобразования — это специфический язык домена < |
Тип<LeftmostLeaf(proto::_child0
)
>является примеромвызывающего преобразования. Это тип функции, который представляет объект функции для вызова и его аргументы. Типы<proto::_child0
>и<proto::_value
>являютсяпримитивными преобразованиями. Это простые структуры, не похожие на функциональные объекты, из которых могут быть составлены вызывающие преобразования. Существует еще один тип преобразования,объект преобразует, с которым мы столкнемся далее.
Первое преобразование, которое мы рассмотрели, просто извлекло значение терминалов. Давайте сделаем то же самое, но на этот раз мы сначала продвигаем все инты в лонгс. (Пожалуйста, простите выдуманность примеров до сих пор; они становятся более интересными позже.) Вот грамматика:
// A simple Proto grammar that matches all terminals, // and a function object that extracts the value from // the terminal, promoting ints to longs: struct ValueWithPomote : proto::or_< proto::when< proto::terminal< int > , long(proto::_value) // <-- an "object" transform > , proto::when< proto::terminal< _ > , proto::_value > > {};
Вы можете прочитать приведенную выше грамматику следующим образом: когда вы соответствуете внутреннему терминалу, извлеките значение из терминала и используйте его для инициализации длинного; в противном случае, когда вы соответствуете другому виду терминала, просто извлеките значение. Тип<long(proto::_value)
>представляет собой так называемоепреобразование объекта. Это похоже на создание временного долгого, но на самом деле это тип функции. Подобно тому, как вызывающее преобразование представляет собой тип функции, который представляет функцию вызова и ее аргументы, объект трансформирует тип функции, который представляет объект для построения, и аргументы для его конструктора.
![]() | Note |
---|---|
Object Transforms vs. Callable Transforms При использовании типов функций в качестве преобразований Proto они могут либо представлять объект для построения, либо функцию для вызова. Это похоже на «нормальный» C++, где синтаксис< LeftmostLeaf(proto::_child0) // <-- a callable transform long(proto::_value) // <-- an object transform Прото вообще не может знать, что есть что, поэтому для дифференциации он использует черту< |
Теперь, когда у нас есть основы преобразования Proto, давайте рассмотрим более реалистичный пример. Мы можем использовать преобразования для повышения безопасности типа калькулятора EDSL. Если вы помните, он позволяет записать арифметические выражения, включающие заполнители аргументов, такие как<_1
>и<_2
>, и передать их алгоритмам STL в качестве функциональных объектов следующим образом:
double a1[4] = { 56, 84, 37, 69 }; double a2[4] = { 65, 120, 60, 70 }; double a3[4] = { 0 }; // Use std::transform() and a calculator expression // to calculate percentages given two input sequences: std::transform(a1, a1+4, a2, a3, (_2 - _1) / _2 * 100);
Это работает потому, что мы дали выражению калькулятора<operator()
>, который оценивает выражение, заменяя заполнителей аргументами<operator()
>. Перегруженный<calculator<>::operator()
>выглядел так:
// Overload operator() to invoke proto::eval() with // our calculator_context. template<typename Expr> double calculator<Expr>::operator()(double a1 = 0, double a2 = 0) const { calculator_context ctx; ctx.args.push_back(a1); ctx.args.push_back(a2); return proto::eval(*this, ctx); }
Хотя это работает, это не идеально, потому что это не предупреждает пользователей, если они предоставляют слишком много или слишком мало аргументов для выражения калькулятора. Рассмотрим следующие ошибки:
(_1 * _1)(4, 2); // Oops, too many arguments! (_2 * _2)(42); // Oops, too few arguments!
Выражение<_1*
_1
>определяет выражение унарного калькулятора; оно принимает один аргумент и квадратует его. Если мы примем более одного аргумента, дополнительные аргументы будут молча игнорироваться, что может удивить пользователей. Следующее выражение<_2
*_2
>определяет выражение двоичного калькулятора; оно принимает два аргумента, игнорирует первый и квадратует второй. Если мы пропустим только один аргумент, код безмолвно заполнит<0.0
>для второго аргумента, что также, вероятно, не соответствует ожиданиям пользователей. Что можно сделать?
Можно сказать, чтоаритмиявыражения калькулятора — это число аргументов, которое он ожидает, и оно равно наибольшему положению в выражении. Так, благодать<_1
*_1
>одна, а благодать<_2
*_2
>— две. Мы можем повысить безопасность типа нашего калькулятора EDSL, убедившись, что частота выражения равна фактическому количеству представленных аргументов. Вычислить аритмию выражения просто с помощью трансформаций Прото.
Легко описать словами, как должна быть рассчитана ловкость выражения. Рассмотрим, что выражения калькулятора могут быть сделаны из<_1
>,<_2
>, буквальных, унарных выражений и двоичных выражений. В следующей таблице показаны условия для каждого из этих 5 компонентов.
Table 29.8. Calculator Sub-Expression Arities
Подвыражение |
Арти |
---|---|
Владелец 1 | < |
Владелец 2 | < |
Буквально | < |
Унарное выражение | подвижность операнда |
Бинарное выражение | Максимальная подвижность двух операндов |
Используя эту информацию, мы можем написать грамматику для выражений калькулятора и прикрепить преобразования для вычисления частоты каждого компонента. Приведенный ниже код вычисляет выражение arity как целое число времени компиляции, используя интегральные обертки и метафункции из библиотеки Boost MPL. Грамматика описана ниже.
struct CalcArity : proto::or_< proto::when< proto::terminal< placeholder<0> >, mpl::int_<1>() > , proto::when< proto::terminal< placeholder<1> >, mpl::int_<2>() > , proto::when< proto::terminal<_>, mpl::int_<0>() > , proto::when< proto::unary_expr<_, CalcArity>, CalcArity(proto::_child) > , proto::when< proto::binary_expr<_, CalcArity, CalcArity>, mpl::max<CalcArity(proto::_left), CalcArity(proto::_right)>() > > {};
Когда мы находим терминал заполнителя или буквальный терминал, мы используем преобразование объекта, такое как<mpl::int_<1>()
>, для создания целого числа времени компиляции (по умолчанию), представляющего собой удобство этого терминала.
Для унарных выражений мы используем<CalcArity(proto::_child)
>, который представляет собойвызывающее вызов преобразование, вычисляющее порядочность ребенка выражения.
Трансформация для бинарных выражений имеет несколько новых приемов. Давайте посмотрим более внимательно:
// Compute the left and right arities and // take the larger of the two. mpl::max<CalcArity(proto::_left), CalcArity(proto::_right)>()
Это преобразование объекта; оно по умолчанию конструирует... что именно? Шаблон<mpl::max<>
>представляет собой метафункцию MPL, которая принимает два целых числа времени компиляции. Он имеет вложенный<::type
>типдеф (не показан), который является максимальным из двух. Но здесь мы, по-видимому, передаем ему две вещи, которые являются, а нецелыми числами времени компиляции; они являются трансформируемыми Прото. Прото достаточно умен, чтобы признать этот факт. Сначала он оценивает две вложенные переменные, вычисляя особенности выражений правого и левого ребенка. Затем он помещает полученные целые числа в<mpl::max<>
>и оценивает метафункцию, запрашивая вложенное<::type
>. Это тип объекта, который строится по умолчанию и возвращается.
В более общем плане, оценивая преобразование объекта, Proto смотрит на тип объекта и проверяет, является ли он шаблонной специализацией, как<mpl::max<>
>. Если это так, Proto ищет вложенные преобразования, которые он может оценить. После того, как любые вложенные преобразования были оценены и заменены обратно в шаблон, новая специализация шаблона является типом результата, если только этот тип не имеет вложенного<::type
>, и в этом случае это становится результатом.
Теперь, когда мы можем рассчитать скорость выражения калькулятора, давайте переопределим обертку выражения<calculator<>
>, которую мы написали в руководстве «Начало работы», чтобы использовать грамматику<CalcArity
>и некоторые макросы из Boost. MPL выдает ошибки компиляции, когда пользователи указывают слишком много или слишком мало аргументов.
// The calculator expression wrapper, as defined in the Hello // Calculator example in the Getting Started guide. It behaves // just like the expression it wraps, but with extra operator() // member functions that evaluate the expression. // NEW: Use the CalcArity grammar to ensure that the correct // number of arguments are supplied. template<typename Expr> struct calculator : proto::extends<Expr, calculator<Expr>, calculator_domain> { typedef proto::extends<Expr, calculator<Expr>, calculator_domain> base_type; calculator(Expr const &expr = Expr()) : base_type(expr) {} typedef double result_type; // Use CalcArity to compute the arity of Expr: static int const arity = boost::result_of<CalcArity(Expr)>::type::value; double operator()() const { BOOST_MPL_ASSERT_RELATION(0, ==, arity); calculator_context ctx; return proto::eval(*this, ctx); } double operator()(double a1) const { BOOST_MPL_ASSERT_RELATION(1, ==, arity); calculator_context ctx; ctx.args.push_back(a1); return proto::eval(*this, ctx); } double operator()(double a1, double a2) const { BOOST_MPL_ASSERT_RELATION(2, ==, arity); calculator_context ctx; ctx.args.push_back(a1); ctx.args.push_back(a2); return proto::eval(*this, ctx); } };
Обратите внимание на использование<boost::result_of<>
>для доступа к типу возврата объекта функции<CalcArity
>. Поскольку мы использовали целые числа времени компиляции в наших трансформациях, частота выражения кодируется в обратном типе объекта функции<CalcArity
>. Протограмматики являются действительными объектами функций в стиле TR1, поэтому вы можете использовать<boost::result_of<>
>для определения их типов возврата.
С нашими утверждениями о времени компиляции, когда пользователи предоставляют слишком много или слишком мало аргументов для выражения калькулятора, например:
(_2 * _2)(42); // Oops, too few arguments!
Они получат сообщение об ошибке компиляции времени на строке с утверждением, что читает что-то подобное.<boost::proto
>:
c:\boost\org\trunk\libs\proto\scratch\main.cpp(97) : error C2664: 'boost::mpl::asse rtion_failed' : cannot convert parameter 1 from 'boost::mpl::failed ************boo st::mpl::assert_relation<x,y,__formal>::************' to 'boost::mpl::assert<false> ::type' with [ x=1, y=2, __formal=bool boost::mpl::operator==(boost::mpl::failed,boost::mpl::failed) ]
Цель этого упражнения состояла в том, чтобы показать, что мы можем написать довольно простую грамматику Прото со встроенными преобразованиями, которая является декларативной и читаемой и может вычислять интересные свойства произвольно сложных выражений. Но преобразования могут сделать больше, чем это. Повышаю. Xpressive использует преобразования для превращения выражений в конечные автоматы состояний для сопоставления регулярных выражений и Boost. Spirit использует преобразования для создания рекурсивных генераторов парсера спуска. Proto поставляется с набором встроенных преобразований, которые вы можете использовать для выполнения очень сложных манипуляций с выражением. В следующих разделах мы увидим некоторые из них в действии.
До сих пор мы видели только примеры грамматики с преобразованиями, которые принимают один аргумент: выражение для преобразования. Но подумайте на мгновение, как в обычном процедурном коде вы превратите двоичное дерево в связанный список. Начнем с пустого списка. Затем вы рекурсивно преобразуете правую ветвь в список и используете результат в качестве начального состояния при преобразовании левой ветви в список. То есть вам понадобится функция, которая принимает два параметра: текущий узел и список до сих пор. Такие проблемы накоплениядовольно распространены при обработке деревьев. Связанный список является примером переменной накопления илисостояния. Каждая итерация алгоритма принимает текущий элемент и состояние, применяет некоторую двоичную функцию к двум и создает новое состояние. В STL этот алгоритм называется<std::accumulate()
>. Во многих других языках он называетсясгиб. Давайте посмотрим, как реализовать алгоритм сгибания с помощью преобразований Proto.
Все грамматики Proto могут необязательно принимать параметр состояния в дополнение к выражению для преобразования. Если вы хотите сложить дерево в список, вам нужно будет использовать параметр состояния, чтобы обойти список, который вы создали до сих пор. Что касается списка, то рост. Библиотека Fusion предоставляет тип<fusion::cons<>
>, из которого можно строить гетерогенные списки. Тип<fusion::nil
>представляет собой пустой список.
Ниже приведена грамматика, которая распознает выходные выражения, такие как<cout_<<
42<<
'\n'
>, и помещает аргументы в список Fusion. Это объясняется ниже.
// Fold the terminals in output statements like // "cout_ << 42 << '\n'" into a Fusion cons-list. struct FoldToList : proto::or_< // Don't add the ostream terminal to the list proto::when< proto::terminal< std::ostream & > , proto::_state > // Put all other terminals at the head of the // list that we're building in the "state" parameter , proto::when< proto::terminal<_> , fusion::cons<proto::_value, proto::_state>( proto::_value, proto::_state ) > // For left-shift operations, first fold the right // child to a list using the current state. Use // the result as the state parameter when folding // the left child to a list. , proto::when< proto::shift_left<FoldToList, FoldToList> , FoldToList( proto::_left , FoldToList(proto::_right, proto::_state) ) > > {};
Прежде чем читать дальше, посмотрите, сможете ли вы применить то, что вы уже знаете об объектных, вызывающих и примитивных преобразованиях, чтобы выяснить, как работает эта грамматика.
Когда вы используете функцию<FoldToList
>, вам нужно будет передать два аргумента: выражение для складывания и начальное состояние: пустой список. Эти два аргумента передаются каждой трансформации. Ранее мы узнали, что<proto::_value
>является примитивным преобразованием, которое принимает терминальное выражение и извлекает его значение. До сих пор мы не знали, что она также принимает текущее состояниеи игнорирует его.<proto::_state
>также является примитивным преобразованием. Она принимает текущее выражение, которое игнорирует, и текущее состояние, которое она возвращает.
Когда мы находим терминал, мы держим его во главе списка минусов, используя текущее состояние в качестве хвоста списка. (Первая альтернатива приводит к пропуску<ostream
>). Мы не хотим<cout
>в списке. Когда мы находим левый сдвиговый узел, мы применяем следующее преобразование:
// Fold the right child and use the result as // state while folding the right. FoldToList( proto::_left , FoldToList(proto::_right, proto::_state) )
Эту трансформацию можно прочитать так: используя текущее состояние, сложите нужного ребенка в список. Используйте новый список как состояние, складывая левого ребенка в список.
![]() | Tip |
---|---|
Если компилятором является Microsoft Visual C++, вы обнаружите, что приведенное выше преобразование не компилируется. Компилятор имеет ошибки с его обработкой вложенных типов функций. Вы можете обойти ошибку, обернув внутреннее преобразование в< FoldToList( proto::_left , proto::call<FoldToList(proto::_right, proto::_state)> ) < |
Теперь, когда мы определили объект функции<FoldToList
>, мы можем использовать его для превращения выходных выражений в списки следующим образом:
proto::terminal<std::ostream &>::type const cout_ = {std::cout}; // This is the type of the list we build below typedef fusion::cons< int , fusion::cons< double , fusion::cons< char , fusion::nil > > > result_type; // Fold an output expression into a Fusion list, using // fusion::nil as the initial state of the transformation. FoldToList to_list; result_type args = to_list(cout_ << 1 << 3.14 << '\n', fusion::nil()); // Now "args" is the list: {1, 3.14, '\n'}
При написании преобразований «складка» является такой базовой операцией, что Proto обеспечивает ряд встроенных сгибаемых преобразований. Мы доберемся до них позже. На данный момент будьте уверены, что вам не всегда придется растягивать свой мозг, чтобы делать такие простые вещи.
В последнем разделе мы увидели, что можем передать второй параметр грамматике с преобразованиями: переменную накопления илисостояние, которое обновляется по мере выполнения преобразования. Бывают случаи, когда ваши преобразования требуют доступа к вспомогательным данным, которые не накапливаются, поэтому связывание их с параметром состояния нецелесообразно. Вместо этого вы можете передавать вспомогательные данные в качестве третьего параметра, известного как параметр.
Давайте изменим наш предыдущий пример так, чтобы он записывал каждый терминал в<std::cout
>, прежде чем поместить его в список. Это может быть удобно для отладки ваших преобразований, например. Мы можем сделать его общим, передав<std::ostream
>в преобразование в параметре данных. В самом преобразовании мы можем восстановить<ostream
>с преобразованием<proto::_data
>. Стратегия такова: используйте преобразование<proto::and_<>
>, чтобы зацепить два действия. Второе действие создаст узел<fusion::cons<>
>, как и раньше. Первое действие, однако, будет отображать текущее выражение. Для этого мы сначала создаем экземпляр<proto::functional::display_expr
>, а затем называем его.
// Fold the terminals in output statements like // "cout_ << 42 << '\n'" into a Fusion cons-list. struct FoldToList : proto::or_< // Don't add the ostream terminal to the list proto::when< proto::terminal< std::ostream & > , proto::_state > // Put all other terminals at the head of the // list that we're building in the "state" parameter , proto::when< proto::terminal<_> , proto::and_< // First, write the terminal to an ostream passed // in the data parameter proto::lazy< proto::make<proto::functional::display_expr(proto::_data)>(_) > // Then, constuct the new cons list. , fusion::cons<proto::_value, proto::_state>( proto::_value, proto::_state ) > > // For left-shift operations, first fold the right // child to a list using the current state. Use // the result as the state parameter when folding // the left child to a list. , proto::when< proto::shift_left<FoldToList, FoldToList> , FoldToList( proto::_left , FoldToList(proto::_right, proto::_state, proto::_data) , proto::_data ) > > {};
Это многое, без сомнения. Но обратите внимание на второй пункт<when
>выше. Когда вы найдете терминал, сначала отобразите терминал, используя<ostream
>, который вы найдете в параметре данных, затем возьмите значение терминала и текущее состояние, чтобы построить новый список<cons
>. Функциональный объект<display_expr
>выполняет работу по печати терминала, а<proto::and_<>
>связывает действия вместе и выполняет их последовательно, возвращая результат последнего.
![]() | Note |
---|---|
Новый< |
Мы можем использовать приведенное выше преобразование, как и раньше, но теперь мы можем пройти<ostream
>в качестве третьего параметра и наблюдать преобразование в действии. Вот пример использования:
proto::terminal<std::ostream &>::type const cout_ = {std::cout}; // This is the type of the list we build below typedef fusion::cons< int , fusion::cons< double , fusion::cons< char , fusion::nil > > > result_type; // Fold an output expression into a Fusion list, using // fusion::nil as the initial state of the transformation. // Pass std::cout as the data parameter so that we can track // the progress of the transform on the console. FoldToList to_list; result_type args = to_list(cout_ << 1 << 3.14 << '\n', fusion::nil(), std::cout); // Now "args" is the list: {1, 3.14, '\n'}
Этот код отображает следующее:
terminal( ) terminal(3.14) terminal(1)
Это довольно окольный способ продемонстрировать, что вы можете передать дополнительные данные для преобразования в качестве третьего параметра. Ограничений на то, каким может быть этот параметр, нет, и, в отличие от государственного параметра, Прото никогда не будет с ним возиться.
![]() | Note |
---|---|
Это продвинутая тема. Не стесняйтесь пропустить, если вы новичок в Proto. |
В приведенном выше примере параметр данных используется в качестве транспортного механизма для неструктурированного набора данных; в этом случае ссылка на<ostream
>. По мере того, как ваши алгоритмы Proto становятся все более сложными, вы можете обнаружить, что с неструктурированным набором данных не очень удобно работать. Различные части вашего алгоритма могут быть заинтересованы в различных битах данных. Вместо этого вам нужен способ передачи в наборепеременных средыпреобразованию, подобно набору пар ключ/значение. Затем вы можете легко получить желаемую часть данных, запросив параметр данных для значения, связанного с конкретным ключом. Прототрансформирует окружающую среду.
Начнем с определения ключа.
BOOST_PROTO_DEFINE_ENV_VAR(mykey_type, mykey);
Это определяет глобальную постоянную<mykey
>с типом<mykey_type
>. Мы можем использовать<mykey
>для хранения части ассоциированных данных в среде преобразования, так как:
// Call the MyEval algorithm with a transform environment containing // two key/value pairs: one for proto::data and one for mykey MyEval()( expr, state, (proto::data = 42, mykey = "hello world") );
Вышеуказанное означает вызов алгоритма<MyEval
>с тремя параметрами: выражением, начальным состоянием и средой преобразования, содержащей две пары ключ/значение.
В рамках алгоритма Proto вы можете получить доступ к значениям, связанным с различными ключами, используя преобразование<proto::_env_var<>
>. Например,<proto::_env_var<mykey_type>
>получил бы значение<"hello world"
>из среды преобразования, созданной выше.
Трансформация<proto::_data
>имеет несколько дополнительных умов. Вместо того, чтобы всегда возвращать третий параметр независимо от того, является ли он каплей или средой преобразования, он сначала проверяет, является ли он каплей или нет. Если это так, то это то, что возвращается. Если нет, он возвращает значение, связанное с ключом<proto::data
>. В приведенном выше примере это будет значение<42
>.
Существует небольшое количество функций, метафункций и классов, которые вы можете использовать для создания и манипулирования средами преобразования, некоторые для проверки того, является ли объект средой преобразования, некоторые для принуждения объекта быть средой преобразования, а некоторые для запроса среды преобразования, имеет ли значение для конкретного ключа. Для исчерпывающего рассмотрения темы ознакомьтесь со ссылкой на заголовок<boost/proto/transform/env.hpp
>.
Давайте воспользуемся<FoldToList
>примером из предыдущих двух разделов, чтобы проиллюстрировать некоторые другие тонкости преобразований Прото. Мы видели, что грамматика при использовании в качестве функциональных объектов может принимать до 3 параметров, и что при использовании этих грамматик в вызывающих преобразованиях также можно указать до 3 параметров. Давайте еще раз взглянем на трансформацию, связанную с нетерминалами из последнего раздела:
FoldToList( proto::_left , FoldToList(proto::_right, proto::_state, proto::_data) , proto::_data )
Здесь мы указываем все три параметра для обоих вызовов грамматики<FoldToList
>. Но нам не нужно указывать все три. Если мы не укажем третий параметр,<proto::_data
>предполагается. Аналогично для второго параметра и<proto::_state
>. Таким образом, приведенное выше преобразование могло быть написано более просто:
FoldToList( proto::_left , StringCopy(proto::_right) )
То же самое относится и к любой примитивной трансформации. Все они эквивалентны:
Table 29.9. Implicit Parameters to Primitive Transforms
Эквивалентные преобразования |
---|
< |
< |
< |
< |
< |
![]() | Note |
---|---|
Грамматики — первичные преобразования — функциональные объекты До сих пор мы говорили, что все грамматики Прото являются функциональными объектами. Но точнее сказать, что грамматика Прото — это примитивные преобразования — особый вид функционального объекта, который принимает от 1 до 3 аргументов, и который Прото знает, как обращаться с ним, особенно когда он используется в вызывающем преобразовании. |
![]() | Note |
---|---|
Не все функциональные объекты являются первичными преобразованиями Теперь у вас может возникнуть соблазн отказаться от параметров< |
Как только вы знаете, что примитивные преобразования всегда будут получать все три параметра — выражение, состояние и данные — это делает возможным то, что не было бы иначе. Например, для двоичных выражений эти два преобразования эквивалентны. Ты видишь почему?
Table 29.10. Two Equivalent Transforms
Без< |
С< |
---|---|
FoldToList( proto::_left , FoldToList(proto::_right, proto::_state, proto::_data) , proto::_data )
|
proto::reverse_fold<_, proto::_state, FoldToList>
|
Обработка выражений с произвольным количеством детей может быть болезненной. Что, если вы хотите что-то сделать с каждым ребенком, то передайте результаты в качестве аргументов какой-то другой функции? Можете ли вы сделать это один раз, не беспокоясь о том, сколько детей имеет выражение? Да. Вот где пригодятся выражения Прото. Распаковка выражений дает вам возможность писать вызывающие и объектные преобразования, которые обрабатываютn-арные выражения.
![]() | Note |
---|---|
Inspired by C++11 Variadic Templates Выражения Proto в распаковке черпают вдохновение из одноименной функции C++11. Если вы знакомы с вариадными функциями и, в частности, с тем, как расширить набор параметров функции, это обсуждение должно показаться очень знакомым. Однако эта функция фактически не использует никаких функций C++11, поэтому описанный здесь код будет работать с любым совместимым компилятором C++98. |
Proto имеет встроенное преобразование<proto::_default<>
>для оценки выражений Proto на языке C++. Но если бы этого не произошло, было бы не слишком сложно реализовать его с нуля, используя шаблоны распаковки Proto. Преобразование<eval
>ниже делает именно это.
// A callable polymorphic function object that takes an unpacked expression // and a tag, and evaluates the expression. A plus tag and two operands adds // them with operator +, for instance. struct do_eval : proto::callable { typedef double result_type; #define UNARY_OP(TAG, OP) \ template<typename Arg> \ double operator()(proto::tag::TAG, Arg arg) const \ { \ return OP arg; \ } \ /**/ #define BINARY_OP(TAG, OP) \ template<typename Left, typename Right> \ double operator()(proto::tag::TAG, Left left, Right right) const \ { \ return left OP right; \ } \ /**/ UNARY_OP(negate, -) BINARY_OP(plus, +) BINARY_OP(minus, -) BINARY_OP(multiplies, *) BINARY_OP(divides, /) /*... others ...*/ }; struct eval : proto::or_< // Evaluate terminals by simply returning their value proto::when<proto::terminal<_>, proto::_value> // Non-terminals are handled by unpacking the expression, // recursively calling eval on each child, and passing // the results along with the expression's tag to do_eval // defined above. , proto::otherwise<do_eval(proto::tag_of<_>(), eval(proto::pack(_))...)> // UNPACKING PATTERN HERE -------------------^^^^^^^^^^^^^^^^^^^^^^^^ > {};
Основная часть вышеупомянутого кода посвящена объекту функции<do_eval
>, который отображает типы тегов на поведение, но интересным битом является определение алгоритма<eval
>внизу. Терминалы обрабатываются довольно просто, но нетерминалы могут быть унарными, двоичными, троичными, дажеn-ary, если рассматривать функции вызова выражений. Алгоритм<eval
>обрабатывает это равномерно с помощью шаблона распаковки.
Нетерминалы оцениваются с помощью этого вызывающего преобразования:
do_eval(proto::tag_of<_>(), eval(proto::pack(_))...)
Вы можете прочитать это так: позвоните объекту функции<do_eval
>с тегом текущего выражения и всем его детям после того, как они были оценены с<eval
>. Образец распаковки - это бит непосредственно перед эллипсом:<eval(proto::pack(_))
>.
Вот что здесь происходит. Выражение распаковки повторяется один раз для каждого ребенка в оцениваемом выражении. В каждом повторении тип<proto::pack(_)
>заменяется на<proto::_child_c<N>
>. Итак, если унарное выражение передано<eval
>, оно на самом деле оценивается так:
// After the unpacking pattern is expanded for a unary expression do_eval(proto::tag_of<_>(), eval(proto::_child_c<0>))
И когда проходит двоичное выражение, шаблон распаковки расширяется следующим образом:
// After the unpacking pattern is expanded for a binary expression do_eval(proto::tag_of<_>(), eval(proto::_child_c<0>), eval(proto::_child_c<1>))
Хотя это не может произойти в нашем примере, при прохождении терминала шаблон распаковки расширяется так, что он извлекает значение из терминала вместо детей. Так что с ним обращаются вот так:
// If a terminal were passed to this transform, Proto would try // to evaluate it like this, which would fail: do_eval(proto::tag_of<_>(), eval(proto::_value))
Это не имеет смысла.<proto::_value
>вернул бы то, что не является выражением Прото, и<eval
>не смог бы его оценить. Алгоритмы Proto не работают, если вы не передаете им выражения Proto.
![]() | Note |
---|---|
Kickin' It Old School Вы можете подумать, мой компилятор не поддерживает шаблоны C++11! Как это может сработать? Ответ прост:< |
Распаковочные узоры очень выразительны. Любое вызывающее или объектное преобразование может быть использовано в качестве распаковочного рисунка, если<proto::pack(_)
>появляется точно один раз где-то внутри него. Это дает вам большую гибкость в том, как вы хотите обрабатывать детей выражения, прежде чем передавать их какому-либо функциональному объекту или конструктору объектов.
![]() | Note |
---|---|
Это продвинутая тема, которая нужна только людям, определяющим большие EDSL. Не стесняйтесь пропустить это, если вы только начинаете с Proto. |
До сих пор мы видели примеры грамматики со встроенными преобразованиями. На практике грамматики могут быть довольно большими, и вы можете использовать их для управления несколькими различными вычислениями. Например, у вас может быть грамматика для линейной области алгебры, и вы можете использовать ее для вычисления формы результата (вектора или матрицы?), а также для оптимального вычисления результата. Вам не нужно копировать и вставлять весь шебанг, чтобы настроить одно из встроенных преобразований. Вместо этого вам нужно определить грамматику один раз и указать преобразования позже, когда вы будете готовы оценить выражение. Для этого вы используетевнешние преобразования. Модель, которую вы будете использовать, такова: заменить одно или несколько преобразований в вашей грамматике специальным заполнителем<proto::external_transform
>. Затем вы создадите набор преобразований, которые вы перейдете к грамматике в параметре данных (3-й параметр после выражения и состояния) при оценке.
Чтобы проиллюстрировать внешние преобразования, мы построим оценщик калькулятора, который может быть настроен на то, чтобы бросить исключение на деление на ноль. Вот передняя часть голой кости, которая определяет домен, грамматику, обертку выражения и некоторые терминалы заполнителя.
#include <boost/assert.hpp> #include <boost/mpl/int.hpp> #include <boost/fusion/container/vector.hpp> #include <boost/fusion/container/generation/make_vector.hpp> #include <boost/proto/proto.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; namespace fusion = boost::fusion; // The argument placeholder type template<typename I> struct placeholder : I {}; // The grammar for valid calculator expressions struct calc_grammar : proto::or_< proto::terminal<placeholder<proto::_> > , proto::terminal<int> , proto::plus<calc_grammar, calc_grammar> , proto::minus<calc_grammar, calc_grammar> , proto::multiplies<calc_grammar, calc_grammar> , proto::divides<calc_grammar, calc_grammar> > {}; template<typename E> struct calc_expr; struct calc_domain : proto::domain<proto::generator<calc_expr> > {}; template<typename E> struct calc_expr : proto::extends<E, calc_expr<E>, calc_domain> { calc_expr(E const &e = E()) : calc_expr::proto_extends(e) {} }; calc_expr<proto::terminal<placeholder<mpl::int_<0> > >::type> _1; calc_expr<proto::terminal<placeholder<mpl::int_<1> > >::type> _2; int main() { // Build a calculator expression, and do nothing with it. (_1 + _2); }
Теперь давайте вставим преобразования в<calc_grammar
>, чтобы мы могли использовать его для оценки выражений калькулятора:
// The calculator grammar with embedded transforms for evaluating expression. struct calc_grammar : proto::or_< proto::when< proto::terminal<placeholder<proto::_> > , proto::functional::at(proto::_state, proto::_value) > , proto::when< proto::terminal<int> , proto::_value > , proto::when< proto::plus<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::minus<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::multiplies<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::divides<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > > {};
С помощью этого определения<calc_grammar
>мы можем оценить выражения, пройдя по вектору Fusion, содержащему значения, используемые для держателей<_1
>и<_2
>:
int result = calc_grammar()(_1 + _2, fusion::make_vector(3, 4)); BOOST_ASSERT(result == 7);
Мы также хотим альтернативную стратегию оценки, которая проверяет деление на ноль и делает исключение. Как нелепо было бы копировать все<calc_grammar
>, просто чтобы изменить одну линию, которая преобразует выражения деления?! Внешние преобразования идеально подходят для этой проблемы.
Во-первых, мы даем правилу деления в нашей грамматике «имя», то есть делаем его структурой. Позже мы используем этот уникальный тип для отправки правильных преобразований.
struct calc_grammar; struct divides_rule : proto::divides<calc_grammar, calc_grammar> {};
Затем мы меняем<calc_grammar
>, чтобы сделать обработку выражений деления внешней.
// The calculator grammar with an external transform for evaluating // division expressions. struct calc_grammar : proto::or_< /* ... as before ... */ , proto::when< divides_rule , proto::external_transform > > {};
Использование<proto::external_transform
>выше делает обработку выражений деления внешне параметризируемой.
Далее мы используем<proto::external_transforms<>
>(обратите внимание на следящие «s»), чтобы захватить нашу стратегию оценки в пакет, который мы можем передать преобразованию в параметре данных. Читайте дальше для объяснения.
// Evaluate division nodes as before struct non_checked_division : proto::external_transforms< proto::when< divides_rule, proto::_default<calc_grammar> > > {}; /* ... */ non_checked_division non_checked; int result2 = calc_grammar()(_1 / _2, fusion::make_vector(6, 2), non_checked);
Структура<non_cecked_division
>связывает преобразование<proto::_default<calc_grammar>
>с правилом грамматики<divides_rule
>. Пример этой структуры передается в качестве третьего параметра при вызове<calc_grammar
>.
Теперь, давайте реализуем проверенное подразделение. Остальное должно быть неудивительно.
struct division_by_zero : std::exception {}; struct do_checked_divide : proto::callable { typedef int result_type; int operator()(int left, int right) const { if (right == 0) throw division_by_zero(); return left / right; } }; struct checked_division : proto::external_transforms< proto::when< divides_rule , do_checked_divide(calc_grammar(proto::_left), calc_grammar(proto::_right)) > > {}; /* ... */ try { checked_division checked; int result3 = calc_grammar_extern()(_1 / _2, fusion::make_vector(6, 0), checked); } catch(division_by_zero) { std::cout << "caught division by zero!\n"; }
Приведенный выше код показывает, как можно использовать единую грамматику с различными преобразованиями, указанными внешне. Это позволяет повторно использовать грамматику для нескольких различных вычислений.
Как описано выше, функция внешних преобразований узурпирует параметр данных, который предназначен для того, чтобы быть местом, где можно передавать произвольные данные, и придает ему определенный смысл. Но что, если вы уже используете параметр данных для чего-то другого? Ответ заключается в использовании трансформирующей среды. Связывая внешние преобразования с ключом<proto::transforms
>, вы можете передавать произвольные данные в других слотах.
Чтобы продолжить приведенный выше пример, что, если нам также необходимо передать часть данных в наше преобразование вместе с внешними преобразованиями? Это будет выглядеть так:
int result3 = calc_grammar_extern()( _1 / _2 , fusion::make_vector(6, 0) , (proto::data = 42, proto::transforms = checked) );
В приведенном выше вызове алгоритма<calc_grammar_extern
>карта внешних преобразований связана с ключом<proto::transforms
>и передается алгоритму в среде преобразования. Также в среде преобразования находится пара ключ/значение, которая связывает значение<42
>с ключом<proto::data
>.
Примитивные преобразования являются строительными блоками для более интересных составных преобразований. Прото определяет кучу полезных примитивных преобразований. Они кратко излагаются ниже.
proto::_value
При наличии терминального выражения возвращайте значение терминала.
proto::_child_c<>
Учитывая не терминальное выражение,<proto::_child_c<
возвращает<N
>>N
>— ребенка.
proto::_child
Синонимы к<proto::_child_c<0>
>.
proto::_left
Синонимы к<proto::_child_c<0>
>.
proto::_right
Синонимы к<proto::_child_c<1>
>.
proto::_expr
Возвращает текущее выражение без изменений.
proto::_state
Возвращает текущее состояние без изменений.
proto::_data
Возвращает текущие данные без изменений.
proto::call<>
Для данного вызывающего преобразования<
,<CT
>proto::call<
превращает вызывающее преобразование в примитивное преобразование. Это полезно для разбора вызывающих преобразований из преобразований объектов, а также для работы с ошибками компилятора с вложенными типами функций.CT
>>
proto::make<>
Для данного объекта преобразование<
,<OT
>proto::make<
превращает объект преобразование в примитивное преобразование. Это полезно для раздвоения преобразований объектов из вызывающих преобразований, а также для работы с ошибками компилятора с вложенными типами функций.OT
>>
proto::_default<>
Учитывая грамматику<G
>,прото::_default<<
оценивает текущий узел в соответствии со стандартным значением C++ операции, которую представляет узел. Например, если текущий узел представляет собой двоичный плюс узел, оба ребенка будут оценены в соответствии сG
>><
, и результаты будут добавлены и возвращены. Тип возврата выводится с помощью Boost. Тип библиотеки.G
>
proto::fold<>
Учитывая три преобразования<
,<ET
>
и<ST
>
,<FT
>proto::fold<
сначала оценивает<ET
>,,>
для получения начального состояния для сгиба, а затем оценивает<ET
>
для каждого элемента в последовательности, чтобы генерировать следующее состояние из предыдущего.ST
>
proto::reverse_fold<>
Как и<proto::fold<>
>, за исключением элементов в последовательности Fusion, они повторяются в обратном порядке.
proto::fold_tree<>
Как<proto::fold<
, за исключением того, что результат<ET
>,<ST
>,<FT
>>
преобразование рассматривается как дерево выражения, которое сплющенодля генерации последовательности, которая должна быть сложена. Сплющивание дерева экспрессии приводит к тому, что узлы ребенка с тем же типом тега, что и родитель, вводятся в последовательность. Например,<ET
>a
>>b
>>c
>будет сплющен на последовательность [<a
>,<b
>,<c
>], и эта последовательность будет сложена.
proto::reverse_fold_tree<>
Как и<proto::fold_tree<>
>, за исключением того, что сплющенная последовательность повторяется в обратном порядке.
proto::lazy<>
Комбинация<proto::make<>
>и<proto::call<>
>, которая полезна, когда характер преобразования зависит от экспрессии, состояния и/или параметров данных.<proto::lazy<R(A0,A1...An)>
>сначала оценивает<proto::make<R()>
>для вычисления вызывающего типа<R2
>. Затем он оценивает<proto::call<R2(A0,A1...An)>
>.
В дополнение к вышеупомянутым примитивным преобразованиям, все элементы грамматики Прото также являются примитивными преобразованиями. Их поведение описано ниже.
proto::_
Верните текущее выражение без изменений.
proto::or_<>
Для указанного набора альтернативных подграмм найдите ту, которая соответствует данному выражению, и примените связанное с ним преобразование.
proto::and_<>
Для данного набора подграмм применяют все связанные с ним преобразования и возвращают результат последнего.
proto::not_<>
Верните текущее выражение без изменений.
proto::if_<>
Учитывая три преобразования, оцените первое и отнеситесь к результату как к булевому значению времени компиляции. Если это так, то оцените второе преобразование. В противном случае оцените третью.
proto::switch_<>
Как и в 2014 году, найдите подграмму, которая соответствует данному выражению, и примените связанное с ним преобразование.
proto::terminal<>
Возврат текущего терминального выражения без изменений.
proto::plus<>
, proto::nary_expr<>
,
et. al.Протограмматика, которая соответствует нетерминалу, такому как<proto::plus<
, при использовании в качестве примитивного преобразования, создает новый узел плюс, где левый ребенок трансформируется в соответствии с<G0
>,<G1
>>
и правый ребенок сG0
><
.G1
>
Обратите внимание на примитивное преобразование, связанное с элементами грамматики, такими как<proto::plus<>
>. Они обладают так называемымпереходнымпреобразованием. Переходное преобразование принимает выражение определенного типа тега (скажем,<proto::tag::plus
>) и создает новое выражение того же типа тега, где каждое детское выражение трансформируется в соответствии с соответствующей детской грамматикой переходного преобразования. Например, эта грамматика...
proto::function< X, proto::vararg<Y> >
... соответствует функциональным выражениям, где первый ребенок соответствует грамматике<X
>, а остальные соответствуют грамматике<Y
>. При использовании в качестве преобразования грамматика создает новое функциональное выражение, где первый ребенок трансформируется в соответствии с<X
>, а остальные трансформируются в соответствии с<Y
>.
Следующие шаблоны классов в Proto можно использовать в качестве грамматики с переходными преобразованиями:
Table 29.11. Class Templates With Pass-Through Transforms
Шаблоны с переходными трансформациями |
---|
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
< |
proto::bitwise_and_assign<> |
< |
< |
< |
< |
< |
< |
< |
< |
Мы видели, что такие шаблоны, как<proto::terminal<>
>,<proto::plus<>
>и<proto::nary_expr<>
>, заполняют многие роли. Это метафункция, которая генерирует типы экспрессии. Это грамматики, которые соответствуют типам выражения. Это примитивные преобразования. Следующие образцы кода показывают примеры каждого из них.
As Metafunctions ...
// proto::terminal<> and proto::plus<> are metafunctions // that generate expression types: typedef proto::terminal<int>::type int_; typedef proto::plus<int_, int_>::type plus_; int_ i = {42}, j = {24}; plus_ p = {i, j};
As Grammars ...
// proto::terminal<> and proto::plus<> are grammars that // match expression types struct Int : proto::terminal<int> {}; struct Plus : proto::plus<Int, Int> {}; BOOST_MPL_ASSERT(( proto::matches< int_, Int > )); BOOST_MPL_ASSERT(( proto::matches< plus_, Plus > ));
As Primitive Transforms ...
// A transform that removes all unary_plus nodes in an expression struct RemoveUnaryPlus : proto::or_< proto::when< proto::unary_plus<RemoveUnaryPlus> , RemoveUnaryPlus(proto::_child) > // Use proto::terminal<> and proto::nary_expr<> // both as grammars and as primitive transforms. , proto::terminal<_> , proto::nary_expr<_, proto::vararg<RemoveUnaryPlus> > > {}; int main() { proto::literal<int> i(0); proto::display_expr( +i - +(i - +i) ); proto::display_expr( RemoveUnaryPlus()( +i - +(i - +i) ) ); }
Вышеприведенный код отображает следующее, которое показывает, что унари плюс узлы были лишены выражения:
minus( unary_plus( terminal(0) ) , unary_plus( minus( terminal(0) , unary_plus( terminal(0) ) ) ) ) minus( terminal(0) , minus( terminal(0) , terminal(0) ) )
В предыдущих разделах мы видели, как составлять большие преобразования из меньших преобразований с использованием типов функций. Меньшие преобразования, из которых состоят более крупные преобразования, являютсяпримитивными преобразованиями, и Прото обеспечивает кучу общих, таких как<_child0
>и<_value
>. В этом разделе мы рассмотрим, как создать свои собственные примитивные преобразования.
![]() | Note |
---|---|
Есть несколько причин, по которым вы можете захотеть написать свои собственные примитивные преобразования. Например, ваше преобразование может быть сложным, и составление его из примитивов становится громоздким. Возможно, вам также придется работать с ошибками компилятора на унаследованных компиляторах, которые делают сложные преобразования с использованием типов функций проблематичными. Наконец, вы также можете определить свои собственные примитивные преобразования, чтобы улучшить время компиляции. Поскольку Прото может просто вызвать примитивное преобразование напрямую без необходимости обрабатывать аргументы или дифференцировать вызывающие преобразования от преобразований объектов, примитивные преобразования более эффективны. |
Примитивные преобразования наследуют от<proto::transform<>
>и имеют вложенный<impl<>
>шаблон, который наследует от<proto::transform_impl<>
>. Например, именно так Прото определяет<_child_c<
преобразование, возвращающее<N
>>N
>— дитя текущего выражения:
namespace boost { namespace proto { // A primitive transform that returns N-th child // of the current expression. template<int N> struct _child_c : transform<_child_c<N> > { template<typename Expr, typename State, typename Data> struct impl : transform_impl<Expr, State, Data> { typedef typename result_of::child_c<Expr, N>::type result_type; result_type operator ()( typename impl::expr_param expr , typename impl::state_param state , typename impl::data_param data ) const { return proto::child_c<N>(expr); } }; }; // Note that _child_c<N> is callable, so that // it can be used in callable transforms, as: // _child_c<0>(_child_c<1>) template<int N> struct is_callable<_child_c<N> > : mpl::true_ {}; }}
Базовый класс<proto::transform<>
>обеспечивает перегрузки<operator()
>и вложенный шаблон<result<>
>, которые делают ваше преобразование действительным функциональным объектом. Они реализованы с точки зрения вложенного шаблона<impl<>
>, который вы определяете.
Базовый класс<proto::transform_impl<>
>— удобство. Он предоставляет некоторые вложенные типдефы, которые обычно полезны. Они указаны в таблице ниже:
Table 29.12. proto::transform_impl<Expr, State, Data> typedefs
печатать |
эквивалентный |
---|---|
< | < |
state | < |
< | < |
< | < |
< | < |
< | < |
Вы заметите, что<_child_c::impl::operator()
>принимает аргументы типов<expr_param
>,<state_param
>и<data_param
>. Типдефы позволяют легко принимать аргументы путем ссылки или соответственно ссылки.
Единственный интересный бит — специализация<is_callable<>
>, которая будет описана вследующем разделе.
Преобразования обычно имеют форму<proto::when<Something,R(A0,A1,...)>
>. Вопрос в том, представляет ли<R
>функцию вызова или объект для построения, и ответ определяет, как<proto::when<>
>оценивает преобразование.<proto::when<>
>использует<proto::is_callable<>
>черту, чтобы разъяснить между ними. Proto делает все возможное, чтобы угадать, можно ли назвать тип или нет, но это не всегда правильно. Лучше всего знать правила использования Proto, чтобы вы знали, когда вам нужно быть более откровенным.
Для большинства типов<R
>,<proto::is_callable<R>
>проверяет наследование<proto::callable
>. Однако, если тип<R
>является шаблонной специализацией, Прото предполагает, что оннеможет называться, даже если шаблон наследует от<proto::callable
>. Посмотрим, почему через минуту. Рассмотрим следующий ошибочный вызывающий объект:
// Proto can't tell this defines something callable! template<typename T> struct times2 : proto::callable { typedef T result_type; T operator()(T i) const { return i * 2; } }; // ERROR! This is not going to multiply the int by 2: struct IntTimes2 : proto::when< proto::terminal<int> , times2<int>(proto::_value) > {};
Проблема в том, что Прото не знает, что<times2<int>
>вызывается, поэтому, скорее, ссылаясь на объект функции<times2<int>
>, Прото попытается построить объект<times2<int>
>и инициализировать его<int
>. Это не будет компиляцией.
![]() | Note |
---|---|
Почему Прото не может сказать, что< |
Есть несколько вариантов решения проблемы<times2<int>
>. Одно из решений состоит в том, чтобы обернуть преобразование в<proto::call<>
>. Это заставляет Прото относиться к<times2<int>
>как к вызывающему:
// OK, calls times2<int> struct IntTimes2 : proto::when< proto::terminal<int> , proto::call<times2<int>(proto::_value)> > {};
Это может быть немного больно, потому что нам нужно обернуть каждое использование<times2<int>
>, которое может быть утомительным и склонным к ошибкам, и делает нашу грамматику загроможденной и трудно читаемой.
Другое решение — специализироваться<proto::is_callable<>
>на нашем<times2<>
>шаблоне:
namespace boost { namespace proto { // Tell Proto that times2<> is callable template<typename T> struct is_callable<times2<T> > : mpl::true_ {}; }} // OK, times2<> is callable struct IntTimes2 : proto::when< proto::terminal<int> , times2<int>(proto::_value) > {};
Это лучше, но все же боль из-за необходимости открыть пространство имен Прото.
Вы можете просто убедиться, что вызывающий тип не является шаблонной специализацией. Рассмотрим следующее:
// No longer a template specialization! struct times2int : times2<int> {}; // OK, times2int is callable struct IntTimes2 : proto::when< proto::terminal<int> , times2int(proto::_value) > {};
Это работает, потому что теперь Прото может сказать, что<times2int
>наследует (косвенно) от<proto::callable
>. Любые типы, не являющиеся шаблонами, могут быть безопасно проверены на наследование, потому что, поскольку они не являются шаблонами, не стоит беспокоиться об ошибках.
Есть один последний способ сказать Прото, что<times2<>
>можно вызвать. Вы можете добавить дополнительный параметр шаблона, который по умолчанию<proto::callable
>:
// Proto will recognize this as callable template<typename T, typename Callable = proto::callable> struct times2 : proto::callable { typedef T result_type; T operator()(T i) const { return i * 2; } }; // OK, this works! struct IntTimes2 : proto::when< proto::terminal<int> , times2<int>(proto::_value) > {};
Обратите внимание, что в дополнение к дополнительному параметру шаблона<times2<>
>все еще наследуется от<proto::callable
>. Это не обязательно в этом примере, но это хороший стиль, потому что любые типы, полученные из<times2<>
>(как<times2int
>определено выше), все равно будут считаться вызывающими.
proto::extends<>
Пример кода стоит тысячи слов.
Тривиальный пример, который создает и оценивает шаблон выражения.
//////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> // This #include is only needed for compilers that use typeof emulation: #include <boost/typeof/std/ostream.hpp> namespace proto = boost::proto; proto::terminal< std::ostream & >::type cout_ = {std::cout}; template< typename Expr > void evaluate( Expr const & expr ) { proto::default_context ctx; proto::eval(expr, ctx); } int main() { evaluate( cout_ << "hello" << ',' << " world" ); return 0; }
Простой пример, который создает миниатюрный встроенный доменно-специфический язык для ленивых арифметических выражений с заполнителями аргументов в стиле TR1.
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This is a simple example of how to build an arithmetic expression // evaluator with placeholders. #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> namespace proto = boost::proto; using proto::_; template<int I> struct placeholder {}; // Define some placeholders proto::terminal< placeholder< 1 > >::type const _1 = {{}}; proto::terminal< placeholder< 2 > >::type const _2 = {{}}; // Define a calculator context, for evaluating arithmetic expressions struct calculator_context : proto::callable_context< calculator_context const > { // The values bound to the placeholders double d[2]; // The result of evaluating arithmetic expressions typedef double result_type; explicit calculator_context(double d1 = 0., double d2 = 0.) { d[0] = d1; d[1] = d2; } // Handle the evaluation of the placeholder terminals template<int I> double operator ()(proto::tag::terminal, placeholder<I>) const { return d[ I - 1 ]; } }; template<typename Expr> double evaluate( Expr const &expr, double d1 = 0., double d2 = 0. ) { // Create a calculator context with d1 and d2 substituted for _1 and _2 calculator_context const ctx(d1, d2); // Evaluate the calculator expression with the calculator_context return proto::eval(expr, ctx); } int main() { // Displays "5" std::cout << evaluate( _1 + 2.0, 3.0 ) << std::endl; // Displays "6" std::cout << evaluate( _1 * _2, 3.0, 2.0 ) << std::endl; // Displays "0.5" std::cout << evaluate( (_1 - _2) / _2, 3.0, 2.0 ) << std::endl; return 0; }
Расширение примера Calc1, который использует<proto::extends<>
>для создания выражений калькулятора допустимых объектов функций, которые могут быть использованы с алгоритмами STL.
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This example enhances the simple arithmetic expression evaluator // in calc1.cpp by using proto::extends to make arithmetic // expressions immediately evaluable with operator (), a-la a // function object #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> namespace proto = boost::proto; using proto::_; template<typename Expr> struct calculator_expression; // Tell proto how to generate expressions in the calculator_domain struct calculator_domain : proto::domain<proto::generator<calculator_expression> > {}; // Will be used to define the placeholders _1 and _2 template<int I> struct placeholder {}; // Define a calculator context, for evaluating arithmetic expressions // (This is as before, in calc1.cpp) struct calculator_context : proto::callable_context< calculator_context const > { // The values bound to the placeholders double d[2]; // The result of evaluating arithmetic expressions typedef double result_type; explicit calculator_context(double d1 = 0., double d2 = 0.) { d[0] = d1; d[1] = d2; } // Handle the evaluation of the placeholder terminals template<int I> double operator ()(proto::tag::terminal, placeholder<I>) const { return d[ I - 1 ]; } }; // Wrap all calculator expressions in this type, which defines // operator () to evaluate the expression. template<typename Expr> struct calculator_expression : proto::extends<Expr, calculator_expression<Expr>, calculator_domain> { explicit calculator_expression(Expr const &expr = Expr()) : calculator_expression::proto_extends(expr) {} BOOST_PROTO_EXTENDS_USING_ASSIGN(calculator_expression<Expr>) // Override operator () to evaluate the expression double operator ()() const { calculator_context const ctx; return proto::eval(*this, ctx); } double operator ()(double d1) const { calculator_context const ctx(d1); return proto::eval(*this, ctx); } double operator ()(double d1, double d2) const { calculator_context const ctx(d1, d2); return proto::eval(*this, ctx); } }; // Define some placeholders (notice they're wrapped in calculator_expression<>) calculator_expression<proto::terminal< placeholder< 1 > >::type> const _1; calculator_expression<proto::terminal< placeholder< 2 > >::type> const _2; // Now, our arithmetic expressions are immediately executable function objects: int main() { // Displays "5" std::cout << (_1 + 2.0)( 3.0 ) << std::endl; // Displays "6" std::cout << ( _1 * _2 )( 3.0, 2.0 ) << std::endl; // Displays "0.5" std::cout << ( (_1 - _2) / _2 )( 3.0, 2.0 ) << std::endl; return 0; }
Расширение примера Calc2, использующее преобразование Прото для вычисления аритмии выражения калькулятора и статически утверждающее, что принято правильное число аргументов.
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This example enhances the arithmetic expression evaluator // in calc2.cpp by using a proto transform to calculate the // number of arguments an expression requires and using a // compile-time assert to guarantee that the right number of // arguments are actually specified. #include <iostream> #include <boost/mpl/int.hpp> #include <boost/mpl/assert.hpp> #include <boost/mpl/min_max.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> #include <boost/proto/transform.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; // Will be used to define the placeholders _1 and _2 template<typename I> struct placeholder : I {}; // This grammar basically says that a calculator expression is one of: // - A placeholder terminal // - Some other terminal // - Some non-terminal whose children are calculator expressions // In addition, it has transforms that say how to calculate the // expression arity for each of the three cases. struct CalculatorGrammar : proto::or_< // placeholders have a non-zero arity ... proto::when< proto::terminal< placeholder<_> >, proto::_value > // Any other terminals have arity 0 ... , proto::when< proto::terminal<_>, mpl::int_<0>() > // For any non-terminals, find the arity of the children and // take the maximum. This is recursive. , proto::when< proto::nary_expr<_, proto::vararg<_> > , proto::fold<_, mpl::int_<0>(), mpl::max<CalculatorGrammar, proto::_state>() > > > {}; // Simple wrapper for calculating a calculator expression's arity. // It specifies mpl::int_<0> as the initial state. The data, which // is not used, is mpl::void_. template<typename Expr> struct calculator_arity : boost::result_of<CalculatorGrammar(Expr)> {}; template<typename Expr> struct calculator_expression; // Tell proto how to generate expressions in the calculator_domain struct calculator_domain : proto::domain<proto::generator<calculator_expression> > {}; // Define a calculator context, for evaluating arithmetic expressions // (This is as before, in calc1.cpp and calc2.cpp) struct calculator_context : proto::callable_context< calculator_context const > { // The values bound to the placeholders double d[2]; // The result of evaluating arithmetic expressions typedef double result_type; explicit calculator_context(double d1 = 0., double d2 = 0.) { d[0] = d1; d[1] = d2; } // Handle the evaluation of the placeholder terminals template<typename I> double operator ()(proto::tag::terminal, placeholder<I>) const { return d[ I() - 1 ]; } }; // Wrap all calculator expressions in this type, which defines // operator () to evaluate the expression. template<typename Expr> struct calculator_expression : proto::extends<Expr, calculator_expression<Expr>, calculator_domain> { typedef proto::extends<Expr, calculator_expression<Expr>, calculator_domain> base_type; explicit calculator_expression(Expr const &expr = Expr()) : base_type(expr) {} BOOST_PROTO_EXTENDS_USING_ASSIGN(calculator_expression<Expr>) // Override operator () to evaluate the expression double operator ()() const { // Assert that the expression has arity 0 BOOST_MPL_ASSERT_RELATION(0, ==, calculator_arity<Expr>::type::value); calculator_context const ctx; return proto::eval(*this, ctx); } double operator ()(double d1) const { // Assert that the expression has arity 1 BOOST_MPL_ASSERT_RELATION(1, ==, calculator_arity<Expr>::type::value); calculator_context const ctx(d1); return proto::eval(*this, ctx); } double operator ()(double d1, double d2) const { // Assert that the expression has arity 2 BOOST_MPL_ASSERT_RELATION(2, ==, calculator_arity<Expr>::type::value); calculator_context const ctx(d1, d2); return proto::eval(*this, ctx); } }; // Define some placeholders (notice they're wrapped in calculator_expression<>) calculator_expression<proto::terminal< placeholder< mpl::int_<1> > >::type> const _1; calculator_expression<proto::terminal< placeholder< mpl::int_<2> > >::type> const _2; // Now, our arithmetic expressions are immediately executable function objects: int main() { // Displays "5" std::cout << (_1 + 2.0)( 3.0 ) << std::endl; // Displays "6" std::cout << ( _1 * _2 )( 3.0, 2.0 ) << std::endl; // Displays "0.5" std::cout << ( (_1 - _2) / _2 )( 3.0, 2.0 ) << std::endl; // This won't compile because the arity of the // expression doesn't match the number of arguments // ( (_1 - _2) / _2 )( 3.0 ); return 0; }
В этом примере построена мини-библиотека для линейной алгебры, использующая шаблоны выражений для устранения необходимости в временных интервалах при добавлении векторов чисел.
В этом примере используется домен с грамматикой для обрезания набора перегруженных операторов. Допускаются только те операторы, которые производят действительные ленивые векторные выражения.
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This example constructs a mini-library for linear algebra, using // expression templates to eliminate the need for temporaries when // adding vectors of numbers. // // This example uses a domain with a grammar to prune the set // of overloaded operators. Only those operators that produce // valid lazy vector expressions are allowed. #include <vector> #include <iostream> #include <boost/mpl/int.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; template<typename Expr> struct lazy_vector_expr; // This grammar describes which lazy vector expressions // are allowed; namely, vector terminals and addition // and subtraction of lazy vector expressions. struct LazyVectorGrammar : proto::or_< proto::terminal< std::vector<_> > , proto::plus< LazyVectorGrammar, LazyVectorGrammar > , proto::minus< LazyVectorGrammar, LazyVectorGrammar > > {}; // Tell proto that in the lazy_vector_domain, all // expressions should be wrapped in laxy_vector_expr<> // and must conform to the lazy vector grammar. struct lazy_vector_domain : proto::domain<proto::generator<lazy_vector_expr>, LazyVectorGrammar> {}; // Here is an evaluation context that indexes into a lazy vector // expression, and combines the result. template<typename Size = std::size_t> struct lazy_subscript_context { lazy_subscript_context(Size subscript) : subscript_(subscript) {} // Use default_eval for all the operations ... template<typename Expr, typename Tag = typename Expr::proto_tag> struct eval : proto::default_eval<Expr, lazy_subscript_context> {}; // ... except for terminals, which we index with our subscript template<typename Expr> struct eval<Expr, proto::tag::terminal> { typedef typename proto::result_of::value<Expr>::type::value_type result_type; result_type operator ()( Expr const & expr, lazy_subscript_context & ctx ) const { return proto::value( expr )[ ctx.subscript_ ]; } }; Size subscript_; }; // Here is the domain-specific expression wrapper, which overrides // operator [] to evaluate the expression using the lazy_subscript_context. template<typename Expr> struct lazy_vector_expr : proto::extends<Expr, lazy_vector_expr<Expr>, lazy_vector_domain> { lazy_vector_expr( Expr const & expr = Expr() ) : lazy_vector_expr::proto_extends( expr ) {} // Use the lazy_subscript_context<> to implement subscripting // of a lazy vector expression tree. template< typename Size > typename proto::result_of::eval< Expr, lazy_subscript_context<Size> >::type operator []( Size subscript ) const { lazy_subscript_context<Size> ctx(subscript); return proto::eval(*this, ctx); } }; // Here is our lazy_vector terminal, implemented in terms of lazy_vector_expr template< typename T > struct lazy_vector : lazy_vector_expr< typename proto::terminal< std::vector<T> >::type > { typedef typename proto::terminal< std::vector<T> >::type expr_type; lazy_vector( std::size_t size = 0, T const & value = T() ) : lazy_vector_expr<expr_type>( expr_type::make( std::vector<T>( size, value ) ) ) {} // Here we define a += operator for lazy vector terminals that // takes a lazy vector expression and indexes it. expr[i] here // uses lazy_subscript_context<> under the covers. template< typename Expr > lazy_vector &operator += (Expr const & expr) { std::size_t size = proto::value(*this).size(); for(std::size_t i = 0; i < size; ++i) { proto::value(*this)[i] += expr[i]; } return *this; } }; int main() { // lazy_vectors with 4 elements each. lazy_vector< double > v1( 4, 1.0 ), v2( 4, 2.0 ), v3( 4, 3.0 ); // Add two vectors lazily and get the 2nd element. double d1 = ( v2 + v3 )[ 2 ]; // Look ma, no temporaries! std::cout << d1 << std::endl; // Subtract two vectors and add the result to a third vector. v1 += v2 - v3; // Still no temporaries! std::cout << '{' << v1[0] << ',' << v1[1] << ',' << v1[2] << ',' << v1[3] << '}' << std::endl; // This expression is disallowed because it does not conform // to the LazyVectorGrammar //(v2 + v3) += v1; return 0; }
Это простой пример произвольных манипуляций с трансформациями Proto. Он принимает некоторое выражение, включающее первичные цвета, и объединяет цвета в соответствии с произвольными правилами. Это порт примера RGB изPETE.
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This is a simple example of doing arbitrary type manipulations with proto // transforms. It takes some expression involving primary colors and combines // the colors according to arbitrary rules. It is a port of the RGB example // from PETE (http://www.codesourcery.com/pooma/download.html). #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/transform.hpp> namespace proto = boost::proto; struct RedTag { friend std::ostream &operator <<(std::ostream &sout, RedTag) { return sout << "This expression is red."; } }; struct BlueTag { friend std::ostream &operator <<(std::ostream &sout, BlueTag) { return sout << "This expression is blue."; } }; struct GreenTag { friend std::ostream &operator <<(std::ostream &sout, GreenTag) { return sout << "This expression is green."; } }; typedef proto::terminal<RedTag>::type RedT; typedef proto::terminal<BlueTag>::type BlueT; typedef proto::terminal<GreenTag>::type GreenT; struct Red; struct Blue; struct Green; /////////////////////////////////////////////////////////////////////////////// // A transform that produces new colors according to some arbitrary rules: // red & green give blue, red & blue give green, blue and green give red. struct Red : proto::or_< proto::plus<Green, Blue> , proto::plus<Blue, Green> , proto::plus<Red, Red> , proto::terminal<RedTag> > {}; struct Green : proto::or_< proto::plus<Red, Blue> , proto::plus<Blue, Red> , proto::plus<Green, Green> , proto::terminal<GreenTag> > {}; struct Blue : proto::or_< proto::plus<Red, Green> , proto::plus<Green, Red> , proto::plus<Blue, Blue> , proto::terminal<BlueTag> > {}; struct RGB : proto::or_< proto::when< Red, RedTag() > , proto::when< Blue, BlueTag() > , proto::when< Green, GreenTag() > > {}; template<typename Expr> void printColor(Expr const & expr) { int i = 0; // dummy state and data parameter, not used std::cout << RGB()(expr, i, i) << std::endl; } int main() { printColor(RedT() + GreenT()); printColor(RedT() + GreenT() + BlueT()); printColor(RedT() + (GreenT() + BlueT())); return 0; }
В этом примере построена мини-библиотека для линейной алгебры, использующая шаблоны выражений для устранения необходимости в временных интервалах при добавлении массивов чисел. Он дублирует пример TArray изPETE.
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This example constructs a mini-library for linear algebra, using // expression templates to eliminate the need for temporaries when // adding arrays of numbers. It duplicates the TArray example from // PETE (http://www.codesourcery.com/pooma/download.html) #include <iostream> #include <boost/mpl/int.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; // This grammar describes which TArray expressions // are allowed; namely, int and array terminals // plus, minus, multiplies and divides of TArray expressions. struct TArrayGrammar : proto::or_< proto::terminal< int > , proto::terminal< int[3] > , proto::plus< TArrayGrammar, TArrayGrammar > , proto::minus< TArrayGrammar, TArrayGrammar > , proto::multiplies< TArrayGrammar, TArrayGrammar > , proto::divides< TArrayGrammar, TArrayGrammar > > {}; template<typename Expr> struct TArrayExpr; // Tell proto that in the TArrayDomain, all // expressions should be wrapped in TArrayExpr<> and // must conform to the TArrayGrammar struct TArrayDomain : proto::domain<proto::generator<TArrayExpr>, TArrayGrammar> {}; // Here is an evaluation context that indexes into a TArray // expression, and combines the result. struct TArraySubscriptCtx : proto::callable_context< TArraySubscriptCtx const > { typedef int result_type; TArraySubscriptCtx(std::ptrdiff_t i) : i_(i) {} // Index array terminals with our subscript. Everything // else will be handled by the default evaluation context. int operator ()(proto::tag::terminal, int const (&data)[3]) const { return data[this->i_]; } std::ptrdiff_t i_; }; // Here is an evaluation context that prints a TArray expression. struct TArrayPrintCtx : proto::callable_context< TArrayPrintCtx const > { typedef std::ostream &result_type; TArrayPrintCtx() {} std::ostream &operator ()(proto::tag::terminal, int i) const { return std::cout << i; } std::ostream &operator ()(proto::tag::terminal, int const (&arr)[3]) const { return std::cout << '{' << arr[0] << ", " << arr[1] << ", " << arr[2] << '}'; } template<typename L, typename R> std::ostream &operator ()(proto::tag::plus, L const &l, R const &r) const { return std::cout << '(' << l << " + " << r << ')'; } template<typename L, typename R> std::ostream &operator ()(proto::tag::minus, L const &l, R const &r) const { return std::cout << '(' << l << " - " << r << ')'; } template<typename L, typename R> std::ostream &operator ()(proto::tag::multiplies, L const &l, R const &r) const { return std::cout << l << " * " << r; } template<typename L, typename R> std::ostream &operator ()(proto::tag::divides, L const &l, R const &r) const { return std::cout << l << " / " << r; } }; // Here is the domain-specific expression wrapper, which overrides // operator [] to evaluate the expression using the TArraySubscriptCtx. template<typename Expr> struct TArrayExpr : proto::extends<Expr, TArrayExpr<Expr>, TArrayDomain> { typedef proto::extends<Expr, TArrayExpr<Expr>, TArrayDomain> base_type; TArrayExpr( Expr const & expr = Expr() ) : base_type( expr ) {} // Use the TArraySubscriptCtx to implement subscripting // of a TArray expression tree. int operator []( std::ptrdiff_t i ) const { TArraySubscriptCtx const ctx(i); return proto::eval(*this, ctx); } // Use the TArrayPrintCtx to display a TArray expression tree. friend std::ostream &operator <<(std::ostream &sout, TArrayExpr<Expr> const &expr) { TArrayPrintCtx const ctx; return proto::eval(expr, ctx); } }; // Here is our TArray terminal, implemented in terms of TArrayExpr // It is basically just an array of 3 integers. struct TArray : TArrayExpr< proto::terminal< int[3] >::type > { explicit TArray( int i = 0, int j = 0, int k = 0 ) { (*this)[0] = i; (*this)[1] = j; (*this)[2] = k; } // Here we override operator [] to give read/write access to // the elements of the array. (We could use the TArrayExpr // operator [] if we made the subscript context smarter about // returning non-const reference when appropriate.) int &operator [](std::ptrdiff_t i) { return proto::value(*this)[i]; } int const &operator [](std::ptrdiff_t i) const { return proto::value(*this)[i]; } // Here we define a operator = for TArray terminals that // takes a TArray expression. template< typename Expr > TArray &operator =(Expr const & expr) { // proto::as_expr<TArrayDomain>(expr) is the same as // expr unless expr is an integer, in which case it // is made into a TArrayExpr terminal first. return this->assign(proto::as_expr<TArrayDomain>(expr)); } template< typename Expr > TArray &printAssign(Expr const & expr) { *this = expr; std::cout << *this << " = " << expr << std::endl; return *this; } private: template< typename Expr > TArray &assign(Expr const & expr) { // expr[i] here uses TArraySubscriptCtx under the covers. (*this)[0] = expr[0]; (*this)[1] = expr[1]; (*this)[2] = expr[2]; return *this; } }; int main() { TArray a(3,1,2); TArray b; std::cout << a << std::endl; std::cout << b << std::endl; b[0] = 7; b[1] = 33; b[2] = -99; TArray c(a); std::cout << c << std::endl; a = 0; std::cout << a << std::endl; std::cout << b << std::endl; std::cout << c << std::endl; a = b + c; std::cout << a << std::endl; a.printAssign(b+c*(b + 3*c)); return 0; }
Это простой пример использования<proto::extends<>
>для расширения терминального типа с дополнительным поведением и использования пользовательских контекстов и<proto::eval()
>для оценки выражений. Это порт примера Vec3 изPETE.
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This is a simple example using proto::extends to extend a terminal type with // additional behaviors, and using custom contexts and proto::eval for // evaluating expressions. It is a port of the Vec3 example // from PETE (http://www.codesourcery.com/pooma/download.html). #include <iostream> #include <functional> #include <boost/assert.hpp> #include <boost/mpl/int.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> #include <boost/proto/proto_typeof.hpp> #include <boost/proto/transform.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; // Here is an evaluation context that indexes into a Vec3 // expression, and combines the result. struct Vec3SubscriptCtx : proto::callable_context< Vec3SubscriptCtx const > { typedef int result_type; Vec3SubscriptCtx(int i) : i_(i) {} // Index array terminals with our subscript. Everything // else will be handled by the default evaluation context. int operator ()(proto::tag::terminal, int const (&arr)[3]) const { return arr[this->i_]; } int i_; }; // Here is an evaluation context that counts the number // of Vec3 terminals in an expression. struct CountLeavesCtx : proto::callable_context< CountLeavesCtx, proto::null_context > { CountLeavesCtx() : count(0) {} typedef void result_type; void operator ()(proto::tag::terminal, int const(&)[3]) { ++this->count; } int count; }; struct iplus : std::plus<int>, proto::callable {}; // Here is a transform that does the same thing as the above context. // It demonstrates the use of the std::plus<> function object // with the fold transform. With minor modifications, this // transform could be used to calculate the leaf count at compile // time, rather than at runtime. struct CountLeaves : proto::or_< // match a Vec3 terminal, return 1 proto::when<proto::terminal<int[3]>, mpl::int_<1>() > // match a terminal, return int() (which is 0) , proto::when<proto::terminal<_>, int() > // fold everything else, using std::plus<> to add // the leaf count of each child to the accumulated state. , proto::otherwise< proto::fold<_, int(), iplus(CountLeaves, proto::_state) > > > {}; // Here is the Vec3 struct, which is a vector of 3 integers. struct Vec3 : proto::extends<proto::terminal<int[3]>::type, Vec3> { explicit Vec3(int i=0, int j=0, int k=0) { (*this)[0] = i; (*this)[1] = j; (*this)[2] = k; } int &operator [](int i) { return proto::value(*this)[i]; } int const &operator [](int i) const { return proto::value(*this)[i]; } // Here we define a operator = for Vec3 terminals that // takes a Vec3 expression. template< typename Expr > Vec3 &operator =(Expr const & expr) { typedef Vec3SubscriptCtx const CVec3SubscriptCtx; (*this)[0] = proto::eval(proto::as_expr(expr), CVec3SubscriptCtx(0)); (*this)[1] = proto::eval(proto::as_expr(expr), CVec3SubscriptCtx(1)); (*this)[2] = proto::eval(proto::as_expr(expr), CVec3SubscriptCtx(2)); return *this; } // This copy-assign is needed because a template is never // considered for copy assignment. Vec3 &operator=(Vec3 const &that) { (*this)[0] = that[0]; (*this)[1] = that[1]; (*this)[2] = that[2]; return *this; } void print() const { std::cout << '{' << (*this)[0] << ", " << (*this)[1] << ", " << (*this)[2] << '}' << std::endl; } }; // The count_leaves() function uses the CountLeaves transform and // to count the number of leaves in an expression. template<typename Expr> int count_leaves(Expr const &expr) { // Count the number of Vec3 terminals using the // CountLeavesCtx evaluation context. CountLeavesCtx ctx; proto::eval(expr, ctx); // This is another way to count the leaves using a transform. int i = 0; BOOST_ASSERT( CountLeaves()(expr, i, i) == ctx.count ); return ctx.count; } int main() { Vec3 a, b, c; c = 4; b[0] = -1; b[1] = -2; b[2] = -3; a = b + c; a.print(); Vec3 d; BOOST_PROTO_AUTO(expr1, b + c); d = expr1; d.print(); int num = count_leaves(expr1); std::cout << num << std::endl; BOOST_PROTO_AUTO(expr2, b + 3 * c); num = count_leaves(expr2); std::cout << num << std::endl; BOOST_PROTO_AUTO(expr3, b + c * d); num = count_leaves(expr3); std::cout << num << std::endl; return 0; }
Это пример использования<BOOST_PROTO_DEFINE_OPERATORS()
>для Protofy-выражений с использованием<std::vector<>
>, типа non-Proto. Это порт векторного примера изPETE.
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This is an example of using BOOST_PROTO_DEFINE_OPERATORS to Protofy // expressions using std::vector<>, a non-proto type. It is a port of the // Vector example from PETE (http://www.codesourcery.com/pooma/download.html). #include <vector> #include <iostream> #include <stdexcept> #include <boost/mpl/bool.hpp> #include <boost/proto/core.hpp> #include <boost/proto/debug.hpp> #include <boost/proto/context.hpp> #include <boost/utility/enable_if.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; using proto::_; template<typename Expr> struct VectorExpr; // Here is an evaluation context that indexes into a std::vector // expression and combines the result. struct VectorSubscriptCtx { VectorSubscriptCtx(std::size_t i) : i_(i) {} // Unless this is a vector terminal, use the // default evaluation context template<typename Expr, typename EnableIf = void> struct eval : proto::default_eval<Expr, VectorSubscriptCtx const> {}; // Index vector terminals with our subscript. template<typename Expr> struct eval< Expr , typename boost::enable_if< proto::matches<Expr, proto::terminal<std::vector<_, _> > > >::type > { typedef typename proto::result_of::value<Expr>::type::value_type result_type; result_type operator ()(Expr &expr, VectorSubscriptCtx const &ctx) const { return proto::value(expr)[ctx.i_]; } }; std::size_t i_; }; // Here is an evaluation context that verifies that all the // vectors in an expression have the same size. struct VectorSizeCtx { VectorSizeCtx(std::size_t size) : size_(size) {} // Unless this is a vector terminal, use the // null evaluation context template<typename Expr, typename EnableIf = void> struct eval : proto::null_eval<Expr, VectorSizeCtx const> {}; // Index array terminals with our subscript. Everything // else will be handled by the default evaluation context. template<typename Expr> struct eval< Expr , typename boost::enable_if< proto::matches<Expr, proto::terminal<std::vector<_, _> > > >::type > { typedef void result_type; result_type operator ()(Expr &expr, VectorSizeCtx const &ctx) const { if(ctx.size_ != proto::value(expr).size()) { throw std::runtime_error("LHS and RHS are not compatible"); } } }; std::size_t size_; }; // A grammar which matches all the assignment operators, // so we can easily disable them. struct AssignOps : proto::switch_<struct AssignOpsCases> {}; // Here are the cases used by the switch_ above. struct AssignOpsCases { template<typename Tag, int D = 0> struct case_ : proto::not_<_> {}; template<int D> struct case_< proto::tag::plus_assign, D > : _ {}; template<int D> struct case_< proto::tag::minus_assign, D > : _ {}; template<int D> struct case_< proto::tag::multiplies_assign, D > : _ {}; template<int D> struct case_< proto::tag::divides_assign, D > : _ {}; template<int D> struct case_< proto::tag::modulus_assign, D > : _ {}; template<int D> struct case_< proto::tag::shift_left_assign, D > : _ {}; template<int D> struct case_< proto::tag::shift_right_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_and_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_or_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_xor_assign, D > : _ {}; }; // A vector grammar is a terminal or some op that is not an // assignment op. (Assignment will be handled specially.) struct VectorGrammar : proto::or_< proto::terminal<_> , proto::and_<proto::nary_expr<_, proto::vararg<VectorGrammar> >, proto::not_<AssignOps> > > {}; // Expressions in the vector domain will be wrapped in VectorExpr<> // and must conform to the VectorGrammar struct VectorDomain : proto::domain<proto::generator<VectorExpr>, VectorGrammar> {}; // Here is VectorExpr, which extends a proto expr type by // giving it an operator [] which uses the VectorSubscriptCtx // to evaluate an expression with a given index. template<typename Expr> struct VectorExpr : proto::extends<Expr, VectorExpr<Expr>, VectorDomain> { explicit VectorExpr(Expr const &expr) : proto::extends<Expr, VectorExpr<Expr>, VectorDomain>(expr) {} // Use the VectorSubscriptCtx to implement subscripting // of a Vector expression tree. typename proto::result_of::eval<Expr const, VectorSubscriptCtx const>::type operator []( std::size_t i ) const { VectorSubscriptCtx const ctx(i); return proto::eval(*this, ctx); } }; // Define a trait type for detecting vector terminals, to // be used by the BOOST_PROTO_DEFINE_OPERATORS macro below. template<typename T> struct IsVector : mpl::false_ {}; template<typename T, typename A> struct IsVector<std::vector<T, A> > : mpl::true_ {}; namespace VectorOps { // This defines all the overloads to make expressions involving // std::vector to build expression templates. BOOST_PROTO_DEFINE_OPERATORS(IsVector, VectorDomain) typedef VectorSubscriptCtx const CVectorSubscriptCtx; // Assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &assign(std::vector<T, A> &arr, Expr const &expr) { VectorSizeCtx const size(arr.size()); proto::eval(proto::as_expr<VectorDomain>(expr), size); // will throw if the sizes don't match for(std::size_t i = 0; i < arr.size(); ++i) { arr[i] = proto::as_expr<VectorDomain>(expr)[i]; } return arr; } // Add-assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &operator +=(std::vector<T, A> &arr, Expr const &expr) { VectorSizeCtx const size(arr.size()); proto::eval(proto::as_expr<VectorDomain>(expr), size); // will throw if the sizes don't match for(std::size_t i = 0; i < arr.size(); ++i) { arr[i] += proto::as_expr<VectorDomain>(expr)[i]; } return arr; } } int main() { using namespace VectorOps; int i; const int n = 10; std::vector<int> a,b,c,d; std::vector<double> e(n); for (i = 0; i < n; ++i) { a.push_back(i); b.push_back(2*i); c.push_back(3*i); d.push_back(i); } VectorOps::assign(b, 2); VectorOps::assign(d, a + b * c); a += if_else(d < 30, b, c); VectorOps::assign(e, c); e += e - 4 / (c + 1); for (i = 0; i < n; ++i) { std::cout << " a(" << i << ") = " << a[i] << " b(" << i << ") = " << b[i] << " c(" << i << ") = " << c[i] << " d(" << i << ") = " << d[i] << " e(" << i << ") = " << e[i] << std::endl; } }
Это пример использования<BOOST_PROTO_DEFINE_OPERATORS()
>для выражений Protofy с использованием<std::vector<>
>и<std::list<>
>типов non-Proto. Это порт смешанного примера изPETE.
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This is an example of using BOOST_PROTO_DEFINE_OPERATORS to Protofy // expressions using std::vector<> and std::list, non-proto types. It is a port // of the Mixed example from PETE. // (http://www.codesourcery.com/pooma/download.html). #include <list> #include <cmath> #include <vector> #include <complex> #include <iostream> #include <stdexcept> #include <boost/proto/core.hpp> #include <boost/proto/debug.hpp> #include <boost/proto/context.hpp> #include <boost/proto/transform.hpp> #include <boost/utility/enable_if.hpp> #include <boost/typeof/std/list.hpp> #include <boost/typeof/std/vector.hpp> #include <boost/typeof/std/complex.hpp> #include <boost/type_traits/remove_reference.hpp> namespace proto = boost::proto; namespace mpl = boost::mpl; using proto::_; template<typename Expr> struct MixedExpr; template<typename Iter> struct iterator_wrapper { typedef Iter iterator; explicit iterator_wrapper(Iter iter) : it(iter) {} mutable Iter it; }; struct begin : proto::callable { template<class Sig> struct result; template<class This, class Cont> struct result<This(Cont)> : proto::result_of::as_expr< iterator_wrapper<typename boost::remove_reference<Cont>::type::const_iterator> > {}; template<typename Cont> typename result<begin(Cont const &)>::type operator ()(Cont const &cont) const { iterator_wrapper<typename Cont::const_iterator> it(cont.begin()); return proto::as_expr(it); } }; // Here is a grammar that replaces vector and list terminals with their // begin iterators struct Begin : proto::or_< proto::when< proto::terminal< std::vector<_, _> >, begin(proto::_value) > , proto::when< proto::terminal< std::list<_, _> >, begin(proto::_value) > , proto::when< proto::terminal<_> > , proto::when< proto::nary_expr<_, proto::vararg<Begin> > > > {}; // Here is an evaluation context that dereferences iterator // terminals. struct DereferenceCtx { // Unless this is an iterator terminal, use the // default evaluation context template<typename Expr, typename EnableIf = void> struct eval : proto::default_eval<Expr, DereferenceCtx const> {}; // Dereference iterator terminals. template<typename Expr> struct eval< Expr , typename boost::enable_if< proto::matches<Expr, proto::terminal<iterator_wrapper<_> > > >::type > { typedef typename proto::result_of::value<Expr>::type IteratorWrapper; typedef typename IteratorWrapper::iterator iterator; typedef typename std::iterator_traits<iterator>::reference result_type; result_type operator ()(Expr &expr, DereferenceCtx const &) const { return *proto::value(expr).it; } }; }; // Here is an evaluation context that increments iterator // terminals. struct IncrementCtx { // Unless this is an iterator terminal, use the // default evaluation context template<typename Expr, typename EnableIf = void> struct eval : proto::null_eval<Expr, IncrementCtx const> {}; // advance iterator terminals. template<typename Expr> struct eval< Expr , typename boost::enable_if< proto::matches<Expr, proto::terminal<iterator_wrapper<_> > > >::type > { typedef void result_type; result_type operator ()(Expr &expr, IncrementCtx const &) const { ++proto::value(expr).it; } }; }; // A grammar which matches all the assignment operators, // so we can easily disable them. struct AssignOps : proto::switch_<struct AssignOpsCases> {}; // Here are the cases used by the switch_ above. struct AssignOpsCases { template<typename Tag, int D = 0> struct case_ : proto::not_<_> {}; template<int D> struct case_< proto::tag::plus_assign, D > : _ {}; template<int D> struct case_< proto::tag::minus_assign, D > : _ {}; template<int D> struct case_< proto::tag::multiplies_assign, D > : _ {}; template<int D> struct case_< proto::tag::divides_assign, D > : _ {}; template<int D> struct case_< proto::tag::modulus_assign, D > : _ {}; template<int D> struct case_< proto::tag::shift_left_assign, D > : _ {}; template<int D> struct case_< proto::tag::shift_right_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_and_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_or_assign, D > : _ {}; template<int D> struct case_< proto::tag::bitwise_xor_assign, D > : _ {}; }; // An expression conforms to the MixedGrammar if it is a terminal or some // op that is not an assignment op. (Assignment will be handled specially.) struct MixedGrammar : proto::or_< proto::terminal<_> , proto::and_< proto::nary_expr<_, proto::vararg<MixedGrammar> > , proto::not_<AssignOps> > > {}; // Expressions in the MixedDomain will be wrapped in MixedExpr<> // and must conform to the MixedGrammar struct MixedDomain : proto::domain<proto::generator<MixedExpr>, MixedGrammar> {}; // Here is MixedExpr, a wrapper for expression types in the MixedDomain. template<typename Expr> struct MixedExpr : proto::extends<Expr, MixedExpr<Expr>, MixedDomain> { explicit MixedExpr(Expr const &expr) : MixedExpr::proto_extends(expr) {} private: // hide this: using proto::extends<Expr, MixedExpr<Expr>, MixedDomain>::operator []; }; // Define a trait type for detecting vector and list terminals, to // be used by the BOOST_PROTO_DEFINE_OPERATORS macro below. template<typename T> struct IsMixed : mpl::false_ {}; template<typename T, typename A> struct IsMixed<std::list<T, A> > : mpl::true_ {}; template<typename T, typename A> struct IsMixed<std::vector<T, A> > : mpl::true_ {}; namespace MixedOps { // This defines all the overloads to make expressions involving // std::vector to build expression templates. BOOST_PROTO_DEFINE_OPERATORS(IsMixed, MixedDomain) struct assign_op { template<typename T, typename U> void operator ()(T &t, U const &u) const { t = u; } }; struct plus_assign_op { template<typename T, typename U> void operator ()(T &t, U const &u) const { t += u; } }; struct minus_assign_op { template<typename T, typename U> void operator ()(T &t, U const &u) const { t -= u; } }; struct sin_ { template<typename Sig> struct result; template<typename This, typename Arg> struct result<This(Arg)> : boost::remove_const<typename boost::remove_reference<Arg>::type> {}; template<typename Arg> Arg operator ()(Arg const &a) const { return std::sin(a); } }; template<typename A> typename proto::result_of::make_expr< proto::tag::function , MixedDomain , sin_ const , A const & >::type sin(A const &a) { return proto::make_expr<proto::tag::function, MixedDomain>(sin_(), boost::ref(a)); } template<typename FwdIter, typename Expr, typename Op> void evaluate(FwdIter begin, FwdIter end, Expr const &expr, Op op) { IncrementCtx const inc = {}; DereferenceCtx const deref = {}; typename boost::result_of<Begin(Expr const &)>::type expr2 = Begin()(expr); for(; begin != end; ++begin) { op(*begin, proto::eval(expr2, deref)); proto::eval(expr2, inc); } } // Add-assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &assign(std::vector<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), assign_op()); return arr; } // Add-assign to a list from some expression. template<typename T, typename A, typename Expr> std::list<T, A> &assign(std::list<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), assign_op()); return arr; } // Add-assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &operator +=(std::vector<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), plus_assign_op()); return arr; } // Add-assign to a list from some expression. template<typename T, typename A, typename Expr> std::list<T, A> &operator +=(std::list<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), plus_assign_op()); return arr; } // Minus-assign to a vector from some expression. template<typename T, typename A, typename Expr> std::vector<T, A> &operator -=(std::vector<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), minus_assign_op()); return arr; } // Minus-assign to a list from some expression. template<typename T, typename A, typename Expr> std::list<T, A> &operator -=(std::list<T, A> &arr, Expr const &expr) { evaluate(arr.begin(), arr.end(), proto::as_expr<MixedDomain>(expr), minus_assign_op()); return arr; } } int main() { using namespace MixedOps; int n = 10; std::vector<int> a,b,c,d; std::list<double> e; std::list<std::complex<double> > f; int i; for(i = 0;i < n; ++i) { a.push_back(i); b.push_back(2*i); c.push_back(3*i); d.push_back(i); e.push_back(0.0); f.push_back(std::complex<double>(1.0, 1.0)); } MixedOps::assign(b, 2); MixedOps::assign(d, a + b * c); a += if_else(d < 30, b, c); MixedOps::assign(e, c); e += e - 4 / (c + 1); f -= sin(0.1 * e * std::complex<double>(0.2, 1.2)); std::list<double>::const_iterator ei = e.begin(); std::list<std::complex<double> >::const_iterator fi = f.begin(); for (i = 0; i < n; ++i) { std::cout << "a(" << i << ") = " << a[i] << " b(" << i << ") = " << b[i] << " c(" << i << ") = " << c[i] << " d(" << i << ") = " << d[i] << " e(" << i << ") = " << *ei++ << " f(" << i << ") = " << *fi++ << std::endl; } }
Демонстрация того, как реализовать<map_list_of()
>от Роста. Назначение библиотеки с помощью Proto.<map_list_assign()
>используется для удобной инициализации<std::map<>
>. Используя Proto, мы можем избежать любого динамического распределения при построении промежуточного представления.
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This is a port of map_list_of() from the Boost.Assign library. // It has the advantage of being more efficient at runtime by not // building any temporary container that requires dynamic allocation. #include <map> #include <string> #include <iostream> #include <boost/proto/core.hpp> #include <boost/proto/transform.hpp> #include <boost/type_traits/add_reference.hpp> namespace proto = boost::proto; using proto::_; struct map_list_of_tag {}; // A simple callable function object that inserts a // (key,value) pair into a map. struct insert : proto::callable { template<typename Sig> struct result; template<typename This, typename Map, typename Key, typename Value> struct result<This(Map, Key, Value)> : boost::add_reference<Map> {}; template<typename Map, typename Key, typename Value> Map &operator()(Map &map, Key const &key, Value const &value) const { map.insert(typename Map::value_type(key, value)); return map; } }; // Work-arounds for Microsoft Visual C++ 7.1 #if BOOST_WORKAROUND(BOOST_MSVC, == 1310) #define MapListOf(x) proto::call<MapListOf(x)> #define _value(x) call<proto::_value(x)> #endif // The grammar for valid map-list expressions, and a // transform that populates the map. struct MapListOf : proto::or_< proto::when< // map_list_of(a,b) proto::function< proto::terminal<map_list_of_tag> , proto::terminal<_> , proto::terminal<_> > , insert( proto::_data , proto::_value(proto::_child1) , proto::_value(proto::_child2) ) > , proto::when< // map_list_of(a,b)(c,d)... proto::function< MapListOf , proto::terminal<_> , proto::terminal<_> > , insert( MapListOf(proto::_child0) , proto::_value(proto::_child1) , proto::_value(proto::_child2) ) > > {}; #if BOOST_WORKAROUND(BOOST_MSVC, == 1310) #undef MapListOf #undef _value #endif template<typename Expr> struct map_list_of_expr; struct map_list_of_dom : proto::domain<proto::pod_generator<map_list_of_expr>, MapListOf> {}; // An expression wrapper that provides a conversion to a // map that uses the MapListOf template<typename Expr> struct map_list_of_expr { BOOST_PROTO_BASIC_EXTENDS(Expr, map_list_of_expr, map_list_of_dom) BOOST_PROTO_EXTENDS_FUNCTION() template<typename Key, typename Value, typename Cmp, typename Al> operator std::map<Key, Value, Cmp, Al> () const { BOOST_MPL_ASSERT((proto::matches<Expr, MapListOf>)); std::map<Key, Value, Cmp, Al> map; return MapListOf()(*this, 0, map); } }; map_list_of_expr<proto::terminal<map_list_of_tag>::type> const map_list_of = {{{}}}; int main() { // Initialize a map: std::map<std::string, int> op = map_list_of ("<", 1) ("<=",2) (">", 3) (">=",4) ("=", 5) ("<>",6) ; std::cout << "\"<\" --> " << op["<"] << std::endl; std::cout << "\"<=\" --> " << op["<="] << std::endl; std::cout << "\">\" --> " << op[">"] << std::endl; std::cout << "\">=\" --> " << op[">="] << std::endl; std::cout << "\"=\" --> " << op["="] << std::endl; std::cout << "\"<>\" --> " << op["<>"] << std::endl; return 0; }
Продвинутый пример преобразования Прото, реализующего конструкцию Говарда Хиннана длябудущих групп, которые блокируют все или некоторые асинхронные операции для завершения и возвращают их результаты в набор соответствующего типа.
// Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This is an example of using Proto transforms to implement // Howard Hinnant's future group proposal. #include <boost/fusion/include/vector.hpp> #include <boost/fusion/include/as_vector.hpp> #include <boost/fusion/include/joint_view.hpp> #include <boost/fusion/include/single_view.hpp> #include <boost/proto/core.hpp> #include <boost/proto/transform.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; namespace fusion = boost::fusion; using proto::_; template<class L,class R> struct pick_left { BOOST_MPL_ASSERT((boost::is_same<L, R>)); typedef L type; }; // Work-arounds for Microsoft Visual C++ 7.1 #if BOOST_WORKAROUND(BOOST_MSVC, == 1310) #define FutureGroup(x) proto::call<FutureGroup(x)> #endif // Define the grammar of future group expression, as well as a // transform to turn them into a Fusion sequence of the correct // type. struct FutureGroup : proto::or_< // terminals become a single-element Fusion sequence proto::when< proto::terminal<_> , fusion::single_view<proto::_value>(proto::_value) > // (a && b) becomes a concatenation of the sequence // from 'a' and the one from 'b': , proto::when< proto::logical_and<FutureGroup, FutureGroup> , fusion::joint_view< boost::add_const<FutureGroup(proto::_left) > , boost::add_const<FutureGroup(proto::_right) > >(FutureGroup(proto::_left), FutureGroup(proto::_right)) > // (a || b) becomes the sequence for 'a', so long // as it is the same as the sequence for 'b'. , proto::when< proto::logical_or<FutureGroup, FutureGroup> , pick_left< FutureGroup(proto::_left) , FutureGroup(proto::_right) >(FutureGroup(proto::_left)) > > {}; #if BOOST_WORKAROUND(BOOST_MSVC, == 1310) #undef FutureGroup #endif template<class E> struct future_expr; struct future_dom : proto::domain<proto::generator<future_expr>, FutureGroup> {}; // Expressions in the future group domain have a .get() // member function that (ostensibly) blocks for the futures // to complete and returns the results in an appropriate // tuple. template<class E> struct future_expr : proto::extends<E, future_expr<E>, future_dom> { explicit future_expr(E const &e) : future_expr::proto_extends(e) {} typename fusion::result_of::as_vector< typename boost::result_of<FutureGroup(E)>::type >::type get() const { return fusion::as_vector(FutureGroup()(*this)); } }; // The future<> type has an even simpler .get() // member function. template<class T> struct future : future_expr<typename proto::terminal<T>::type> { future(T const &t = T()) : future::proto_derived_expr(future::proto_base_expr::make(t)) {} T get() const { return proto::value(*this); } }; // TEST CASES struct A {}; struct B {}; struct C {}; int main() { using fusion::vector; future<A> a; future<B> b; future<C> c; future<vector<A,B> > ab; // Verify that various future groups have the // correct return types. A t0 = a.get(); vector<A, B, C> t1 = (a && b && c).get(); vector<A, C> t2 = ((a || a) && c).get(); vector<A, B, C> t3 = ((a && b || a && b) && c).get(); vector<vector<A, B>, C> t4 = ((ab || ab) && c).get(); return 0; }
Это продвинутый пример, который показывает, как реализовать простую лямбду EDSL с Proto, как Boost. Lambda_library. Он использует контексты, преобразования и расширение выражения.
/////////////////////////////////////////////////////////////////////////////// // Copyright 2008 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This example builds a simple but functional lambda library using Proto. #include <iostream> #include <algorithm> #include <boost/mpl/int.hpp> #include <boost/mpl/min_max.hpp> #include <boost/mpl/eval_if.hpp> #include <boost/mpl/identity.hpp> #include <boost/mpl/next_prior.hpp> #include <boost/fusion/tuple.hpp> #include <boost/typeof/typeof.hpp> #include <boost/typeof/std/ostream.hpp> #include <boost/typeof/std/iostream.hpp> #include <boost/proto/core.hpp> #include <boost/proto/context.hpp> #include <boost/proto/transform.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; namespace fusion = boost::fusion; using proto::_; // Forward declaration of the lambda expression wrapper template<typename T> struct lambda; struct lambda_domain : proto::domain<proto::pod_generator<lambda> > {}; template<typename I> struct placeholder { typedef I arity; }; template<typename T> struct placeholder_arity { typedef typename T::arity type; }; // The lambda grammar, with the transforms for calculating the max arity struct lambda_arity : proto::or_< proto::when< proto::terminal< placeholder<_> > , mpl::next<placeholder_arity<proto::_value> >() > , proto::when< proto::terminal<_> , mpl::int_<0>() > , proto::when< proto::nary_expr<_, proto::vararg<_> > , proto::fold<_, mpl::int_<0>(), mpl::max<lambda_arity, proto::_state>()> > > {}; // The lambda context is the same as the default context // with the addition of special handling for lambda placeholders template<typename Tuple> struct lambda_context : proto::callable_context<lambda_context<Tuple> const> { lambda_context(Tuple const &args) : args_(args) {} template<typename Sig> struct result; template<typename This, typename I> struct result<This(proto::tag::terminal, placeholder<I> const &)> : fusion::result_of::at<Tuple, I> {}; template<typename I> typename fusion::result_of::at<Tuple, I>::type operator ()(proto::tag::terminal, placeholder<I> const &) const { return fusion::at<I>(this->args_); } Tuple args_; }; // The lambda<> expression wrapper makes expressions polymorphic // function objects template<typename T> struct lambda { BOOST_PROTO_BASIC_EXTENDS(T, lambda<T>, lambda_domain) BOOST_PROTO_EXTENDS_ASSIGN() BOOST_PROTO_EXTENDS_SUBSCRIPT() // Calculate the arity of this lambda expression static int const arity = boost::result_of<lambda_arity(T)>::type::value; template<typename Sig> struct result; // Define nested result<> specializations to calculate the return // type of this lambda expression. But be careful not to evaluate // the return type of the nullary function unless we have a nullary // lambda! template<typename This> struct result<This()> : mpl::eval_if_c< 0 == arity , proto::result_of::eval<T const, lambda_context<fusion::tuple<> > > , mpl::identity<void> > {}; template<typename This, typename A0> struct result<This(A0)> : proto::result_of::eval<T const, lambda_context<fusion::tuple<A0> > > {}; template<typename This, typename A0, typename A1> struct result<This(A0, A1)> : proto::result_of::eval<T const, lambda_context<fusion::tuple<A0, A1> > > {}; // Define our operator () that evaluates the lambda expression. typename result<lambda()>::type operator ()() const { fusion::tuple<> args; lambda_context<fusion::tuple<> > ctx(args); return proto::eval(*this, ctx); } template<typename A0> typename result<lambda(A0 const &)>::type operator ()(A0 const &a0) const { fusion::tuple<A0 const &> args(a0); lambda_context<fusion::tuple<A0 const &> > ctx(args); return proto::eval(*this, ctx); } template<typename A0, typename A1> typename result<lambda(A0 const &, A1 const &)>::type operator ()(A0 const &a0, A1 const &a1) const { fusion::tuple<A0 const &, A1 const &> args(a0, a1); lambda_context<fusion::tuple<A0 const &, A1 const &> > ctx(args); return proto::eval(*this, ctx); } }; // Define some lambda placeholders lambda<proto::terminal<placeholder<mpl::int_<0> > >::type> const _1 = {{}}; lambda<proto::terminal<placeholder<mpl::int_<1> > >::type> const _2 = {{}}; template<typename T> lambda<typename proto::terminal<T>::type> const val(T const &t) { lambda<typename proto::terminal<T>::type> that = {{t}}; return that; } template<typename T> lambda<typename proto::terminal<T &>::type> const var(T &t) { lambda<typename proto::terminal<T &>::type> that = {{t}}; return that; } template<typename T> struct construct_helper { typedef T result_type; // for TR1 result_of T operator()() const { return T(); } // Generate BOOST_PROTO_MAX_ARITY overloads of the // following function call operator. #define BOOST_PROTO_LOCAL_MACRO(N, typename_A, A_const_ref, A_const_ref_a, a)\ template<typename_A(N)> \ T operator()(A_const_ref_a(N)) const \ { return T(a(N)); } #define BOOST_PROTO_LOCAL_a BOOST_PROTO_a #include BOOST_PROTO_LOCAL_ITERATE() }; // Generate BOOST_PROTO_MAX_ARITY-1 overloads of the // following construct() function template. #define M0(N, typename_A, A_const_ref, A_const_ref_a, ref_a) \ template<typename T, typename_A(N)> \ typename proto::result_of::make_expr< \ proto::tag::function \ , lambda_domain \ , construct_helper<T> \ , A_const_ref(N) \ >::type const \ construct(A_const_ref_a(N)) \ { \ return proto::make_expr< \ proto::tag::function \ , lambda_domain \ >( \ construct_helper<T>() \ , ref_a(N) \ ); \ } BOOST_PROTO_REPEAT_FROM_TO(1, BOOST_PROTO_MAX_ARITY, M0) #undef M0 struct S { S() {} S(int i, char c) { std::cout << "S(" << i << "," << c << ")\n"; } }; int main() { // Create some lambda objects and immediately // invoke them by applying their operator(): int i = ( (_1 + 2) / 4 )(42); std::cout << i << std::endl; // prints 11 int j = ( (-(_1 + 2)) / 4 )(42); std::cout << j << std::endl; // prints -11 double d = ( (4 - _2) * 3 )(42, 3.14); std::cout << d << std::endl; // prints 2.58 // check non-const ref terminals (std::cout << _1 << " -- " << _2 << '\n')(42, "Life, the Universe and Everything!"); // prints "42 -- Life, the Universe and Everything!" // "Nullary" lambdas work too int k = (val(1) + val(2))(); std::cout << k << std::endl; // prints 3 // check array indexing for kicks int integers[5] = {0}; (var(integers)[2] = 2)(); (var(integers)[_1] = _1)(3); std::cout << integers[2] << std::endl; // prints 2 std::cout << integers[3] << std::endl; // prints 3 // Now use a lambda with an STL algorithm! int rgi[4] = {1,2,3,4}; char rgc[4] = {'a','b','c','d'}; S rgs[4]; std::transform(rgi, rgi+4, rgc, rgs, construct<S>(_1, _2)); return 0; }
Это продвинутый пример, который показывает, как внешне параметризировать преобразования грамматики. Он определяет калькулятор EDSL с грамматикой, которая может выполнять либо проверенную, либо непроверенную арифметику.
// Copyright 2011 Eric Niebler. Distributed under the Boost // Software License, Version 1.0. (See accompanying file // LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) // // This is an example of how to specify a transform externally so // that a single grammar can be used to drive multiple differnt // calculations. In particular, it defines a calculator grammar // that computes the result of an expression with either checked // or non-checked division. #include <iostream> #include <boost/assert.hpp> #include <boost/mpl/int.hpp> #include <boost/mpl/next.hpp> #include <boost/mpl/min_max.hpp> #include <boost/fusion/container/vector.hpp> #include <boost/fusion/container/generation/make_vector.hpp> #include <boost/proto/proto.hpp> namespace mpl = boost::mpl; namespace proto = boost::proto; namespace fusion = boost::fusion; // The argument placeholder type template<typename I> struct placeholder : I {}; // Give each rule in the grammar a "name". This is so that we // can easily dispatch on it later. struct calc_grammar; struct divides_rule : proto::divides<calc_grammar, calc_grammar> {}; // Use external transforms in calc_gramar struct calc_grammar : proto::or_< proto::when< proto::terminal<placeholder<proto::_> > , proto::functional::at(proto::_state, proto::_value) > , proto::when< proto::terminal<proto::convertible_to<double> > , proto::_value > , proto::when< proto::plus<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::minus<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > , proto::when< proto::multiplies<calc_grammar, calc_grammar> , proto::_default<calc_grammar> > // Note that we don't specify how division nodes are // handled here. Proto::external_transform is a placeholder // for an actual transform. , proto::when< divides_rule , proto::external_transform > > {}; template<typename E> struct calc_expr; struct calc_domain : proto::domain<proto::generator<calc_expr> > {}; template<typename E> struct calc_expr : proto::extends<E, calc_expr<E>, calc_domain> { calc_expr(E const &e = E()) : calc_expr::proto_extends(e) {} }; calc_expr<proto::terminal<placeholder<mpl::int_<0> > >::type> _1; calc_expr<proto::terminal<placeholder<mpl::int_<1> > >::type> _2; // Use proto::external_transforms to map from named grammar rules to // transforms. struct non_checked_division : proto::external_transforms< proto::when< divides_rule, proto::_default<calc_grammar> > > {}; struct division_by_zero : std::exception {}; struct do_checked_divide : proto::callable { typedef int result_type; int operator()(int left, int right) const { if (right == 0) throw division_by_zero(); return left / right; } }; // Use proto::external_transforms again, this time to map the divides_rule // to a transforms that performs checked division. struct checked_division : proto::external_transforms< proto::when< divides_rule , do_checked_divide(calc_grammar(proto::_left), calc_grammar(proto::_right)) > > {}; int main() { non_checked_division non_checked; int result2 = calc_grammar()(_1 / _2, fusion::make_vector(6, 2), non_checked); BOOST_ASSERT(result2 == 3); try { checked_division checked; // This should throw int result3 = calc_grammar()(_1 / _2, fusion::make_vector(6, 0), checked); BOOST_ASSERT(false); // shouldn't get here! } catch(division_by_zero) { std::cout << "caught division by zero!\n"; } }
Proto был первоначально разработан как частьBoost.Xpressiveдля упрощения работы по преобразованию шаблона выражения в исполняемую машину конечного состояния, способную соответствовать обычному выражению. С тех пор Proto нашел применение в обновленном и улучшенном Spirit-2 и связанной с ним библиотеке кармы. В результате этих усилий Proto превратилась в общую и абстрактную грамматику и структуру преобразования деревьев, применимую в самых разных сценариях EDSL.
Грамматика и структура преобразования дерева смоделированы на основе грамматики и семантического действия Духа. Структура данных дерева экспрессии во многих отношениях аналогична структурам данных Fusion и совместима с итераторами и алгоритмами Fusion.
Синтаксис для грамматических характеристик соответствия<proto::matches<>
>вдохновлен лямбда-выражениями MPL.
Идея использования типов функций для композитных преобразований Прото вдохновлена нотацией Алексея Гуртового«круглый» лямбда.
Ren, D. and Erwig, M. 2006. A generic recursion toolbox for Haskell or: scrap your boilerplate systematically. In Proceedings of the 2006 ACM SIGPLAN Workshop on Haskell (Portland, Oregon, USA, September 17 - 17, 2006). Haskell '06. ACM, New York, NY, 13-24. DOI=http://doi.acm.org/10.1145/1159842.1159845
Техническая статья о более ранней версии Proto была принята на симпозиум ACM SIGPLAN Symposium on Library-Centric Software Design LCSD'07. http://lcsd.cs.tamu.edu/2007/final/1/1_Paper.pdf. Дерево, описанное в этой статье, отличается от того, что существует сегодня.
Преобразование формы<R(A0,A1,...)
>(т.е. типа функции), где<proto::is_callable<R>::value
>является<true
>.<R
>рассматривается как полиморфный объект функции, а аргументы рассматриваются как преобразования, которые приводят аргументы к объекту функции.
В Прото терминконтекстотносится к объекту, который может быть передан вместе с выражением для оценки функции<proto::eval()
>. Контекст определяет, как оценивается выражение. Все контекстные структуры определяют вложенный<eval<>
>шаблон, который, будучи инстанцирован с типом тега узла (например,<proto::tag::plus
>), является бинарным полиморфным функциональным объектом, который принимает выражение этого типа и контекстного объекта. Таким образом, контексты связывают поведение с узлами экспрессии.
В Прото терминдоменотносится к типу, который связывает выражения внутри этого домена с генераторомдля этого домена и необязательно грамматикойдля домена. Домены используются в основном для того, чтобы наполнить выражения в этом домене дополнительными членами и ограничить перегрузки оператора Proto, так что выражения, не соответствующие грамматике домена, никогда не создаются. Домены — это пустые структуры, которые наследуют от<proto::domain<>
>.
Язык программирования, который нацелен на конкретное проблемное пространство, предоставляя идиомы программирования, абстракции и конструкции, которые соответствуют конструкциям в этом проблемном пространстве.
Язык домена, реализованный как библиотека. Язык, на котором написана библиотека, называется «принимающим» языком, а язык, реализуемый библиотекой, называется «встроенным» языком.
В Прото выражениеявляется гетерогенным деревом, где каждый узел представляет собой либо инстанциацию<boost::proto::expr<>
>,<boost::proto::basic_expr<>
>, либо некоторый тип, который является расширением (через<boost::proto::extends<>
>или<BOOST_PROTO_EXTENDS()
>) такой инстанциации.
Метод C++, использующий шаблоны и перегрузку оператора, чтобы заставить выражения строить деревья, которые представляют собой выражение для ленивой оценки позже, а не для оценки выражения с нетерпением. Некоторые библиотеки C++ используют шаблоны выражения для создания встраиваемых доменных языков.
В Прото генераторявляется унарным полиморфным функциональным объектом, который вы указываете при определении домена. После создания нового выражения Proto передает это выражение генератору вашего домена для дальнейшей обработки. Часто генератор обертывает выражение в обертку расширения, которая добавляет к нему дополнительные элементы.
В Прото грамматикаявляется типом, который описывает подмножество типов экспрессии Прото. Выражения в области должны соответствовать грамматике этой области. Метафункция<proto::matches<>
>оценивает, соответствует ли тип выражения грамматике. Грамматики являются либо примитивными, такими как<proto::_
>, композитами, такими как<proto::plus<>
>, структурами управления, такими как<proto::or_<>
>, либо некоторым типом, полученным из грамматики.
Преобразование формы<R(A0,A1,...)
>(т.е. типа функции), где<proto::is_callable<R>::value
>является<false
>.<R
>рассматривается как тип объекта для построения, а аргументы рассматриваются как преобразования, которые дают параметры конструктору.
Пример типа класса с перегруженным оператором вызова функции и вложенным<result_type
>типдефом или<result<>
>шаблоном для расчета типа возврата оператора вызова функции.
Тип, который определяет вид полиморфного функционального объекта, который принимает три аргумента: выражение, состояние и данные. Примитивные преобразования могут быть использованы для составления так называемых преобразований и объектных преобразований.
Субдомен - это домен, который объявляет другой домен своим супердоменом. Выражения в поддоменах могут быть объединены с выражениями в супердомене, и полученное выражение находится в супердомене.
Трансформаторы используются для манипулирования деревьями экспрессии. Они бывают трех видов: примитивные преобразования, так называемые преобразования или преобразования объектов. Преобразование<
может быть выполнено в тройной полиморфный функциональный объект с<T
>proto::when<>
>, как в<proto::when<proto::_,
. Такой объект функции принимаетвыражение,состояниеиданныепараметры и вычисляет из них результат.T
>>
<#include
<boost/proto/core.hpp>
>Не всегда можно удержать что-то по стоимости. По умолчанию<proto::as_expr()
>делает исключение для функций, абстрактных типов и iostreams (типы, полученные из<std::ios_base
>). Эти объекты удерживаются путем ссылки. Все остальные удерживаются ценностью, даже массивами.
<boost::proto
>Это сообщение об ошибке было создано с помощью Microsoft Visual C++. 9.0. Различные компиляторы будут излучать различные сообщения с различной степенью читаемости.
Статья Users' Guide раздела The Boost C++ Libraries BoostBook Documentation Subset Chapter 29. Boost.Proto может быть полезна для разработчиков на c++ и boost.
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.
:: Главная :: Chapter 29. Boost.Proto ::
реклама |