Boost::Spirit и друзья. Краткий экскурс. Часть 2.

Учимся извлекать из примеров пользу. Новые друзья — атрибуты.

Вот и вторая часть повествования, здесь мне придётся вас маленько огорчить — сразу же переходить ко второму пункту нашего плана я не стану, а прежде расскажу чуть более подробно о синтезируемых атрибутах, это нам очень пригодится в будущем. Понимание всех этих основных принципов для освоения Спирита очень важно, так что я просто не могу обойти эту тему стороной и сразу поместить читателя в гущу событий, чтобы он «как-нибудь там» разбирался с тем, что так внезапно свалилось на голову.

Сигнатура правил и грамматик.

Как я уже говорил, сигнатуры выглядят следующим образом: RT(A1, ..., AN), где: RT — тот атрибут, который в результате возвращает само правило; A1, ..., AN — атрибуты-параметры, то есть те величины, от которых зависит правая часть правила (обычно их называют «наследуемые атрибуты», от англ.: inherited attributes).

Надеюсь, простят мне читатели некоторую вольность, когда я говорю о правилах и грамматиках одновременно, тем временем, упоминая лишь что-то одно: грамматика, грубо говоря, является лишь более высокоуровневым «подобием» правила, семантика у них весьма и весьма схожая, так что я не вижу особого смысла оговаривать каждый случай в отдельности. К слову, в Спирите есть и ещё одна грамматического плана сущность — подправила (subrules), более узкие, нежели обыкновенные правила, но их рассмотрение выходит за рамки данной темы, просто чтобы вы знали, если кому уж очень интересно станет поглядеть самому.

Синтезируемые атрибуты.

Итак, синтезируемые атрибуты — это то, что возвращает нам успешно отработавший парсер. Например, если парсер double_ распознает число с плавающей точкой 1.5, то он вернёт в качестве такого атрибута то самое число 1.5 типа double, что было распознано. В наших правилах мы можем контролировать как атрибуты каждого из парсеров, входящих в правую часть правила, так и синтезируемый атрибут левой части — самого правила. Для манипуляций с этими атрибутами было создано несколько имён-заместителей (англ.: placeholders; то есть таких переменных, подставив которые в нужное место и в нужное время, spirit сам разберется, что вместо заместителя передавать):

  • qi::_val — синтезируемый атрибут левой части правила.
  • qi::_1 — атрибут, содержащий результат работы парсера.
  • qi::_2, qi::_3, ... — дополнительные имена-заместители, предназначенные для различных нужд (например, обработка ошибок).

В основном, с этими placeholder’ами мы имеем дело в семантических действиях, так что сейчас познакомимся и с ними.

Семантические действия.

Каждому парсеру Спирита возможно назначить семантическое действие, которое будет выполнено сразу же, как только успешно отработает парсер, к которому оно присоединено. Запись такая: P[F], где: P — парсер, F — семантическое действие, присоединённое к парсеру P.

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

void F(Attrib const&);
void F(Attrib const&, Context&);
void F(Attrib const&, Context&, bool&);

Последние два параметра нам с вами будут не нужны, так что мы просто не будем их использовать, остановившись на самом первом варианте

void F(Attrib const&);

где принимается ссылка на константный атрибут, полученный от сработавшего парсера.

Для функторов сигнатура следующая:

void operator()(Attrib const&, unused_type, unused_type) const;
void operator()(Attrib const&, Context&, unused_type) const;
void operator()(Attrib const&, Context&, bool&) const;

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

Особенно хорошо выглядят действия с использованием boost::phoenix, но, дабы не создавать лишних сложностей и не валить всё в одну кучу, пока избежим разговора о нём.

Небольшой примерчик для начала:

Например, требуется распознавать строки такого вида: [integer], где integer — целое число.

Мы можем написать примерно следующее для решения этой задачки:

qi::parse(begin, end, '[' >> qi::int_ >> ']');

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

// plain function
void to_log(int const & val)
    {
    std::clog << val << std::endl;
    }

// function object
struct to_log_functor
    {
    void operator() ( int const & val
                    , qi::unused_type
                    , qi::unused_type ) const
        {
        std::clog << val << std::endl;
        }
    };

В случае описания функтора, operator() должен иметь ровно три параметра, от этого никуда не деться, хотя позже мы узнаем, что есть и немного другой способ, который этого не требует, с использованием всё того же boost::phoenix.

Теперь наконец посмотрим, как прицепить выполнение действия при парсинге в первом случае (если у нас есть простая функция, то передавать надо указатель на неё):

qi::parse(begin, end, '[' >> qi::int_[&to_log] >> ']');

и во втором случае, с функтором:

qi::parse(begin, end, '[' >> qi::int_[to_log_functor()] >> ']');

Стоит также упомянуть тот факт, что возможно использовать в качестве действий и функции-члены классов, но тут опять всё тот же феникс возникает, так что нам пока хватит и того, что уже есть.

Кстати, как вы уже, должно быть, заметили, я описывал правило для парсинга прямо в функции parse, что подтверждает то, что абсолютно все сущности spirit::qi, принимающие участие в парсинге, сами трактуются как парсеры и могут быть использованы в любом месте, где ожидается парсер spirit::qi.

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

std::string str = "[1]";
std::string::iterator begin = str.begin(), end = str.end();
int result;
qi::parse(begin, end, '[' >> qi::int_[to_log_functor()] >> ']', result);
// here result = 1;

Если мы попытаемся рефакторить наш код в нормальную грамматику, получим совсем другие результаты:

template <typename Iterator>
struct simple_grammar : qi::grammar<Iterator, int()>
    {
    simple_grammar()
        : simple_grammar::base_type(start)
        {
        start = '[' >> qi::int_[to_log_functor()] >> ']';
        }

    qi::rule<Iterator, int()> start;
    };

...

std::string str = "[1]";
std::string::iterator begin = str.begin(), end = str.end();

simple_grammar<std::string::iterator> gr;
int result;

qi::parse(begin, end, gr, result);
// here result is undefined!

Почему такое произошло, что случилось?! Мы ведь явно написали, что у грамматики есть атрибут целочисленного типа, обеспечили функции parse необходимое число для записи атрибута парсинга туда! А штука в том, что если в правой части правила есть хотя бы одно семантическое действие, то автоматическое присваивание атрибута правой части атрибуту левой игнорируется. В принципе, можно поступить двумя способами: либо написать семантическое действие, которое будет присваивать нужный атрибут левой части (запишется это так: [qi::_val = qi::_1]), либо воспользоваться очень удобной фичей Спирита под названием «автоматические правила» (auto rules). В случае с прямой подстановкой выражения в функцию parse дело сработало именно потому, что там действует автоматическое распространение атрибутов (attribute propagation) по-умолчанию.

Автоматические правила.

Довольно часто можно видеть правила вида R = P[qi::_val = qi::_1], так вот если у нас как раз такое правило и атрибут правой части правила совместим с атрибутом левой части, то можно воспользоваться такой записью: R %= P, так что автоматически атрибут парсера P присваивается атрибуту правила R. Удобно! В нашем случае код начального нетерминала грамматики можно переписать так:

start %= '[' >> qi::int_[to_log_functor()] >> ']';

Что будет эквивалентно такому коду:

start = ('[' >> qi::int_[to_log_functor()] >> ']')[qi::_val = qi::_1];

Теперь всё работает и мы можем каким-либо образом использовать полученные данные, дело сделано. Впрочем, одно-то число распознать не проблема. Как быть, если нам нужно распознать пару чисел, целый список, куда и как их сохранять? Прежде, чем ответить на этот вопрос, ещё попутно освещу один момент, связанный с тем, какие атрибуты могут иметь сложные выражения в spirit.

Атрибуты сложных выражений.

До сих пор мы рассматривали только одиночные атрибуты, каждый сам по себе. А что будет атрибутом выражения "A >> B", "A|B", "*A", например? Вообще говоря, зависит от того, какими атрибутами будут обладать парсеры A и B. Не стану молотить отсебятину, а просто буду приводить отрывочки из официальной документации, которая довольно наглядно показывает, во что превращаются атрибуты парсеров в сложных выражениях. Например:

Sequence attribute propagation rule
a: A, b: B --> (a >> b): tuple<A, B>

Теперь что это означает: если парсер a обладает атрибутом типа A, а парсер b — атрибутом типа B, то результат (a >> b) будет иметь атрибут типа tuple<A, B>. Что за tuple, откуда он взялся? В данном случае, запись tuple<A, B> является просто заместителем любого контейнера библиотеки boost::fusion, который может держать в себе типы A и B, как например boost::fusion::tuple<A, B>, boost::fusion::vector<A, B>, std::pair<A, B>. С fusion особо знакомиться пока не будем, просто знайте, что есть такая библиотека, контейнеры которой используются здесь, чуть позже объясню, почему используются именно они.

В принципе, очевидно, что для того, чтобы предоставляемый тип был совместим с типом атрибута, он должен:

  • либо быть конвертируемым в тип атрибута (например, int конвертируется в double),
  • либо представлять определённую функциональность, в частности, следовать концепции, совместимой со сложным типом атрибута.

Далее будет также использоваться несколько других обозначений: vector<A> — заменитель для любого STL совместимого контейнера, способного держать элементы типа A, Unused заменяет тип unused_type.

Sequence (a >> b):

a: A, b: B --> (a >> b): tuple<A, B>
a: A, b: Unused --> (a >> b): A
a: Unused, b: B --> (a >> b): B
a: Unused, b: Unused --> (a >> b): Unused

a: A, b: A --> (a >> b): vector<A>
a: vector<A>, b: A --> (a >> b): vector<A>
a: A, b: vector<A> --> (a >> b): vector<A>
a: vector<A>, b: vector<A> --> (a >> b): vector<A>
Alternative (a | b):

a: A, b: B --> (a | b): variant<A, B>
a: A, b: Unused --> (a | b): optional<A>
a: A, b: B, c: Unused --> (a | b | c): optional<variant<A, B> >
a: Unused, b: B --> (a | b): optional<B>
a: Unused, b: Unused --> (a | b): Unused
a: A, b: A --> (a | b): A
Kleene (*a):

a: A --> *a: vector<A>
a: Unused --> *a: Unused

Так, ещё какие-то variant и optional появились, это что такое… Не думаю, что должны бы возникнуть такие вопросы, но, тем не менее, расскажу в общих чертах, что за этими названиями скрывается. Variant<A, B> — заменитель для контейнера, способного держать в один момент времени лишь одну сущность, принадлежащую одному из типов, перечисленных в шаблонных параметрах, реальный тип такого контейнера будет "boost::variant<A, B>", эдакий union, только типобезопасный и с кучей разных вкусностей. Optional<A> — заместитель для контейнера, который либо может содержать в себе значение типа A, либо может вообще ничего не содержать(англ.: optional — необязательный), например boost::optional<A>. Unused просто означает то, что тип атрибута данного выражения не используется.

Разберёмся поподробней с тем, что тут происходит. Начнём с оператора следования: если один из атрибутов оператора не используется, то результатом следования будет второй атрибут, если оба типа не используются, то и результат тоже не имеет атрибута, довольно естественное упрощение. Если типы обоих атрибутов представляют собой коллекции из элементов одинаковых типов, то в результате атрибутом станет тоже коллекция элементов того же самого типа. Что это означает на практике: если у a есть атрибут std::vector<int>{1, 2, 3}, а у bstd::vector<int>{4, 5, 6}, то результат a >> b будет вектором std::vector<int>{1, 2, 3, 4, 5, 6}, они просто «склеятся» вместе. А вот когда типы A и B разные и не конвертируются друг в друга неявно, придётся просто «свалить всё это дело в кучу» и использовать контейнер для хранения гетерогенных коллекций данных типа того же tuple<A, B>, как раз поэтому используются контейнеры fusion, которые позволяют содержать элементы разных типов вместе.

После объяснений по поводу variant и optional думаю, что разбор вариантов с оператором альтернативы не вызовет трудностей: все правила абсолютно естественны, единственное, что следует отметить: если A и B — одинаковые типы, то результатом a | b станет просто A, а не какой-нибудь variant<A, A>.

С оператором звёздочки всё просто: контейнер vector<A> последовательно набивается теми элементами, что распознаёт парсер a.

А вот и ещё одна мелочь нарисовалась: кто-то наверняка уже заметил, что, говоря про tuple<A, B>, в качестве реальных примеров контейнеров fusion я упоминал std::pair<A, B> — вроде не похоже на контейнер fusion, ага? Всем нам знакомая пара из стандартной библиотеки. Так-то оно, конечно, так, но этот тип используется довольно часто, в следствие чего для пары решено было сделать совместимые с boost::fusion обёртки «прямо из коробки». Самое время рассмотреть связанные с этим примеры.

Разбор пары значений.

Рассмотрим небольшой пример с парсингом пары чисел вида [integer, integer] в структуру std::pair<int, int>.

Естественно, первое, что мы сделаем с нашим предыдущим примером — захотим переписать всё это дело примерно так:

template <typename Iterator>
struct simple_grammar
    : qi::grammar<Iterator, std::pair<int, int>()>
    {
    simple_grammar()
        : simple_grammar::base_type(start)
        {
        start = '[' >> qi::int_ >> ',' >> qi::int_ >> ']';
        }

    qi::rule<Iterator, std::pair<int, int>()> start;
    };

Теперь посмотрим, что творится: атрибутом правой части правила start будет некий контейнер типа vector<int>, вроде не pair, да?

Если мы попробуем скомпилировать следующий код с нашей грамматикой

std::string str = "[1,2]";
std::string::iterator begin = str.begin(), end = str.end();
simple_grammar<std::string::iterator> gr;
std::pair<int, int> p;
qi::parse(begin, end, gr, p);

то ничего не получится, будут ошибки в стиле 'невозможно преобразовать тип ... в std::pair<int, int>'. Всё из-за того, что std::pair<T1, T2> не является стандартным контейнером fusion и тот тип, который получился в результате операций qi::int_ >> ',' >> qi::int_ невозможно прозрачно преобразовать в std::pair<int, int>. Впрочем, я же что-то говорил про то, что для std::pair<T1, T2> есть обёртка уже в составе самой библиотеки fusion. Какая удача, воспользуемся этим! Всё, что нужно для того, чтобы сделать std::pair<A, B> полноценным членом boost::fusion, надо подключить заголовок

<boost/fusion/include/std_pair.hpp>

Теперь приведённый пример будет компилироваться успешно, так как мы предоставили всё необходимое для того, чтобы fusion смог без посторонней помощи переделать свой тип в std::pair<T1, T2>.

К слову, возможен и такой вариант: функции parse и phrase_parse, как мы видели в предыдущей части из их прототипов, могут принимать в качестве выходных атрибутов N величин (на самом деле, это N устанавливается равным значению препроцессорной константы SPIRIT_ARGUMENTS_LIMIT, которая по-умолчанию равна 10), так вот без необходимости подключать предыдущий заголовок, следующий код отработает вполне успешно:

std::string str = "[1,2]";
std::string::iterator begin = str.begin(), end = str.end();
std::pair<int, int> p;
qi::parse(begin, end, '['
                      >> qi::int_ >> ',' >> qi::int_ >>
                      ']', p.first, p.second);

Таким образом, имеем возможность работать с функциями parse и phrase_parse в printf-подобной манере, иногда это может оказаться очень удобным.

Работа с автоматическим заполнением атрибутов сводится обычно к тому, чтобы адаптировать нашу пользовательскую структуру для того, чтобы она могла считаться полноправным fusion контейнером, ибо большинство производимых спиритом коллекций атрибутов на деле являются fusion последовательностями, а в обычные контейнеры типа std::pair<T1, T2> они уже могут прозрачно переделываться благодаря тому, что это предусмотрено в самой библиотеке fusion.

Теперь мы умеем заполнять пары, основываясь на знаниях об атрибутах. Уже что-то, впрочем, иногда хочется оперировать с последовательностями произвольной длины, то есть иметь на выходе, например, вектор, состоящий из некоторых данных.

Парсинг списка величин.

Рассмотрим такой пример: дана строка вида {<id1>:<num1>, <id2>:<num2>, <id3>:<num3>, ..., <idN>:<numN>} и нам нужно вытащить из неё все входящие в этот набор числа.

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

Более простой вариант мог бы выглядеть так:

bool ParseNumbers( std::string & str
                 , std::vector<int> & nums)
    {
    std::string::iterator  begin = str.begin()
                         , end   = str.end();

    namespace qi = boost::spirit::qi;
    return qi::parse(begin, end,
                (
                '{' >>
                    (
                    qi::omit[*(qi::char_ - ':')] >> ':'
                    >> qi::int_
                    ) % ','
                >> '}'
                ), nums
            ) && begin == end;
    }

Но он плохой в том плане, если вместо нормального идентификатора у нас будет какой-то мусор вплоть до каждого двоеточия — нехорошо. А тем не менее, мы здесь видим нечто новое, об этом стоит рассказать. Первое, что можно заметить — парочка новых операторов. Сначала видим выражение qi::char_ - ':' — это оператор «разности», то есть в данном случае получившийся парсер будет считывать все символы (qi::char_), кроме двоеточия, таким образом, этот оператор позволяет регулировать поведение парсера, говоря ему: «делай то, что должен, только кроме вот этого», иначе, позволяет задать список исключения определённых результатов для некоторого парсера. Дальше видим, что всё это дело обёрнуто в директиву qi::omit[], это директива, которая заставляет игнорировать возвращаемый атрибут парсера, который внутри находится, иными словами, вместо возвращаемого атрибута от парсера мы получим unused_type. То есть: *(qi::char_ - ':') даст атрибут типа vector<char>, но нам эта информация пока ни к чему и мы просто хотим проигнорировать эту поступившую информацию точно так же, как например, игнорируем возвращаемое значение от фигурных скобок для этого выражения или двоеточия — они нам не важны. Итого, что имеем пока:

qi::omit[*(qi::char_ - ':')] >> ':'
>> qi::int_
--> int

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

Дальше нам встречается ещё один новый оператор — оператор списка '%'. Что он делает: пусть дано выражение 'A % B', тогда данный парсер распарсивает список из величин, распознаваемых парсером A, разделённых величинами, распознаваемыми парсером B. Иначе говоря, его действие эквивалентно следующему выражению:

A >> *(B >> A)

Что у нас есть в нашем примере: <идентификатор с двоеточием и число> % ','. Расшифровываем: список из <идентификатор с двоеточием и число>, разделённых между собой запятыми. К слову, атрибутом парсера 'a % b' является vector<A>, где A — тип атрибута для парсера а. В нашем случае, атрибутом этого сложного выражения является атрибут vector<int>, потому что <идентфикатор с двоеточием и число> имеет атрибут типа int, благодаря директиве qi::omit[]. Таким образом, атрибут всего выражения таков:

'{' >>
	(
	qi::omit[*(qi::char_ - ':')] >> ':'
	>> qi::int_
	) % ','
>> '}'
--> vector<int>

В функцию parse мы передаём std::vector<int>, а тип атрибута у нас является STL совместимым контейнером с элементами типа int, так что возможно прозрачное присвоение атрибута нашему результирующему вектору.

Вот, собственно, познакомились с тем, как заполнять пары чисел и векторы, узнали о новых операторах списка (%) и разности (-), увидели новую директиву qi::omit[], позволяющую игнорировать распространение атрибута для парсера.

А теперь посмотрим на более идеологически правильный вариант:

bool ParseNumbers( std::string & str
                 , std::vector<int> & nums)
    {
    std::string::iterator  begin = str.begin()
                         , end   = str.end();

    namespace qi = boost::spirit::qi;
    return qi::phrase_parse(begin, end,
                (
                '{' >>
                    (
                    qi::omit[
                        qi::lexeme[
                            (qi::alpha | '_')
                            >> *(qi::alnum | '_')
                        ]
                    ] >> ':'
                    >> qi::int_
                    ) % ','
                >> '}'
                ), qi::space, nums
            ) && begin == end;
    }

Почти то же самое, за исключением парочки моментов. Первое — мы использовали функцию phrase_parse вместо parse и добавили в качестве стандартного парсера пропусков qi::space, пропускающий самые обыкновенные пробельные символы, включая символы новой строки и табуляции. То есть теперь мы не обязаны писать всю строку исключительно без пробелов, чтобы парсинг проходил успешно, теперь можно так:

{id1:124,id2:222}

или так:

{  id1   :
123 , id2 : 4 }

Второе, что было изменено: считывание идентификатора теперь происходит иначе:

(qi::alpha | '_') >> *(qi::alnum | '_')

qi::alpha распознаёт только буквенные символы, qi::alnum распознаёт буквы и цифры, здесь всё аналогично нотации из регулярных выражений. Также мы должны оставить возможность начинать идентификатор с знака нижнего подчёркивания и использовать его в записи строки далее, короче, строим обычный идентификатор в C-стиле. Но вместе с этим есть и ещё одно изменение: появилась наша старая знакомая — директива lexeme[]:

qi::lexeme[(qi::alpha | '_') >> *(qi::alnum | '_')]

Особо внимательные читатели наверняка помнят, что директива lexeme[] отключает использование парсера пробелов для того парсера, что в эту директиву заключён. Если бы мы оставили считывание идентификатора без этой директивы, то вполне валидными оказались бы идентификаторы вида 'a 1', '_fucking shit', а нам такого не надо, вот мы и запрещаем неявный пропуск пробелов, дабы идентификаторы не содержали в себе пропусков.

Разбор списка пар «ключ=значение».

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

Предположим, что конфигурационный файл имеет следующий формат:

// так обозначаются комментарии -- стандартный C++ стиль
option1;
option2 = value2;
// ...
option_N = value_N;

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

Недолго думая, пишем примерно следующий код:

template <typename Iterator>
struct key_value_grammar
    : qi::grammar<
          Iterator
        , std::map<std::string, std::string>()
        >
    {
    key_value_grammar()
        : key_value_grammar::base_type(pairs)
        {
        pairs = pair % ';' >> ';';
        pair  = key >> -('=' >> value);
        key   = (qi::alpha | qi::char_('_')) >> *(qi::alnum | qi::char_('_'));
        value = +(qi::alnum | qi::char_('_'));
        }

    qi::rule<
          Iterator
        , std::map<std::string, std::string>()
    > pairs;

    qi::rule<
          Iterator
        , std::pair<std::string, std::string>()
    > pair;

    qi::rule<Iterator, std::string()> value, key;
    };

Здесь появилось немного нового. Во-первых, нетерминалы key и value имеют тип std::string, что сразу же даёт понять, что атрибут от правых частей (который представляется в виде некоторого типа vector<char>) может прозрачно преобразовываться в std::string — удобно! Точно такая же фигня и с нетерминалом pairs — его атрибут есть некий тип вроде vector<pair<std::string, std::string> >, так вот этот вектор тоже незаметно может конвертироваться в контейнер std::map<std::string, std::string>. Ещё увидели оператор унарного минуса, о котором я говорил ещё в предыдущей части, обозначающий то же самое, что и оператор '?' в стандартной записи РБНФ, то есть «может встречаться 0 или 1 раз» — это нам обеспечивает возможность не предоставлять ключу значение, если оно не нужно.

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

*(qi::alnum | qi::char_('_'))

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

*qi::char_("a-zA-Z0-9_")

Раз уж речь пошла о символах, то расскажу и ещё об одной полезной фиче. До настоящего момента, когда мы хотели исключить из списка распознаваемых символов какой-то определённый символ, мы пользовались бинарным оператором разности, например, qi::char_ - ':'. Оказывается, сделать исключение некоторых недопустимых символов можно и иначе: с помощью унарного оператора отрицания ‘~’. Применим этот оператор только к символьным парсерам, то есть к таким, которые считывают лишь один символ, как, например, qi::char_, qi::digit, qi::alpha и тому подобные. Так вот, парочка примеров действия данного оператора:

~qi::char_ // не принимает вообще никакие символы
~qi::digit // принимает любые символы, кроме цифр
~qi::char_("a-zA-Z")
// принимает всё за исключением символов,
// входящих в диапазоны a-z и A-Z,
// короче, всё, кроме маленьких и больших английских букв

Добавляем комментарии.

Вроде бы всё есть, да только вот про комментарии совсем забыли! Нам нужно пропускать как обычные пробелы, символы новой строки и табуляции, так и однострочные комментарии, начинающиеся с последовательности «//» и заканчивающиеся концом строки. Можно было бы, конечно, написать велосипед, написав правило для комментариев my_comment и вставляя строчки вида "*(qi::space | my_comment)" перед и после каждого парсера в нашей грамматике, но это ни разу не удобно, да и вообще фигня какая-то. Пойдём другим путём — напишем грамматику пропусков, которая будет учитывать пробельные символы и комментарии и передадим её в качестве шаблона нашей грамматике.

Сначала напишу необходимый код, а далее дам необходимые разъяснения:

namespace qi = boost::spirit::qi;
namespace repository = boost::spirit::repository;

template <typename Iterator>
struct skip_grammar : public qi::grammar<Iterator>
    {
    skip_grammar()
        : skip_grammar::base_type(skip)
        {
        skip
            =   qi::space
            |   repository::confix("//", qi::eol)
                [*(qi::char_ - qi::eol)]
            ;
        }

    qi::rule<Iterator> skip;
    };

Теперь посмотрим, что происходит: в правиле skip мы считываем либо обычные пробельные символы с помощью стандартного парсера qi::space, который этим занимается, либо парсим то, что распознаёт эта интересная конструкция:

repository::confix("//", qi::eol)
[*(qi::char_ - qi::eol)]

Перед нами директива confix(prefix, suffix)[subject], её действие эквивалентно следующему коду:

qi::omit[prefix] >> subject >> qi::omit[suffix]

То есть данная директива даёт возможность заключить парсер subject между двумя парсерами prefix и suffix, при этом их атрибуты отклоняются, а в качестве атрибута выражения возвращается атрибут заключённого посередине парсера subject. Теперь разбираем эту директиву на нашем примере. qi::eol — парсер, распознающий символ конца строки. confix("//", qi::eol) — заключаем парсер между последовательностью символов «//» и концом строки, какой парсер у нас внутри — *(qi::char_ - qi::eol) — то есть абсолютно любые последовательности символов, исключая лишь символ конца строки, как раз то, что нужно для комментария.

В принципе, можно было бы написать и в более развёрнутом варианте с использованием директивы qi::omit[], но, во-первых, раз есть компонент, идеально подходящий под наши цели, то стоит им воспользоваться, а не строить велосипед, а во-вторых, использование confix[] позволяет инкапсулировать парсеры prefix и suffix в отдельную конструкцию, скрывающую ненужные детали, что в самописном варианте не получится сделать столь красиво и экономично, как в случае с confix[].

Кстати, совершенно никакого труда не стоило бы добавить и комментарии в стиле C:

repository::confix("/*", "*/")[*(qi::char_ - "*/")]

Ну и маленькая мелочь: директива confix[] находится в пространстве имён boost::spirit::repository и для того, чтобы она была доступна, нужно подключить заголовок

<boost/spirit/repository/include/qi_confix.hpp>

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

Модифицируем key_value_grammar:

template <typename Iterator>
struct key_value_grammar
    : qi::grammar<
          Iterator
        , std::map<std::string, std::string>()
        , skip_grammar<Iterator>
        >
    {
    key_value_grammar()
        : key_value_grammar::base_type(pairs)
        {
        pairs = pair % ';' >> ';';
        pair  = key >> -('=' >> value);
        key   = (qi::alpha | qi::char_('_')) >> *(qi::alnum | qi::char_('_'));
        value = +(qi::alnum | qi::char_('_'));
        }

    qi::rule<
          Iterator
        , std::map<std::string, std::string>()
        , skip_grammar<Iterator>
    > pairs;

    qi::rule<
          Iterator
        , std::pair<std::string, std::string>()
        , skip_grammar<Iterator>
    > pair;

    qi::rule<Iterator, std::string()> value, key;
    };

Управляющий код:

std::string config;
// fill in 'config' string...
std::string::iterator  begin = config.begin()
                     , end   = config.end();
key_value_grammar<std::string::iterator> gr;
skip_grammar<std::string::iterator> sk;
std::map<std::string, std::string> conf_map;
qi::phrase_parse(begin, end, gr, sk, conf_map);
// ...

Таким образом, для того, чтобы поддерживать парсеры для символов, которые должны считаться пробельными и их атрибуты нам должны быть не важны, нужно:

  1. Написать грамматику, распознающую язык для всего того, что может называться комментарием и атрибуты чего мы должны игнорировать.
  2. Добавить тип пробельной грамматики в заголовки тех правил и грамматик, где данный парсер-пропускатель должен использоваться по-умолчанию.
  3. Передать объект грамматики пробелов вместе с нужной грамматикой в контролирующую функцию phrase_parse.

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

Отключение автоматического парсера пробелов возможно сделать двумя способами: либо директивой lexeme[], о которой мы уже знаем и которой успели воспользоваться, либо директивой no_skip[]. Разница между ними заключается лишь в одном: директива lexeme[] всегда, прежде чем запретить пропуск пробелов, делает предварительный пропуск символов в начале строки, то есть, например, следующий код отработает успешно и мы получим нормально распознанную строку:

std::string expression = "   12345";
std::string::iterator  begin = expression.begin()
                     , end   = expression.end();
std::string integer;
bool success
        = qi::phrase_parse( begin
                          , end
                          , qi::lexeme[
                                -(qi::lit('+') | '-')
                                >> +qi::digit
                            ]
                          , qi::space
                          , integer
                          );
// here: success = true; integer = "12345";

Директива no_skip[] в отличие от lexeme[] никогда не делает предварительный пропуск символов, прежде чем отключить парсер пробелов, во всём остальном она абсолютно идентична директиве lexeme[]. Например, тот же самый код, но с использованием директивы no_skip[] заместо lexeme[] завершится неудачей, потому что пробелы в начале строки не будут автоматически считаны как это было бы сделано в случае с lexeme[]:

std::string expression = "   12345";
std::string::iterator  begin = expression.begin()
                     , end   = expression.end();
std::string integer;
bool success
        = qi::phrase_parse( begin
                          , end
                          , qi::no_skip[
                                -(qi::lit('+') | '-')
                                >> +qi::digit
                            ]
                          , qi::space
                          , integer
                          );
// here: success = false; integer = "";

Выключать пробелы мы научились, теперь буквально пару слов о том, как их можно включать: специально для этого есть директива skip[subject], которая включает обратно автоматический парсер пробелов для парсера subject. Также имеется альтернативный вариант лексемы skip(skip_parser)[subject], которая даёт возможность явно включить заданный парсер пробелов skip_parser для парсера subject в нужном месте.

И снова к нашим баранам…

А вообще, что-то я едва не забыл о нашем калькуляторе! Теперь мы достаточно много знаем об атрибутах и семантических правилах, чтобы написать вариант «интерпретирующего» калькулятора, который прямо по ходу парсинга будет выполнять необходимые арифметические операции.

К слову, в начале этой части я говорил о такой записи: qi::_val = qi::_1, так вот существует возможность использовать не только operator=, также можно пользоваться целым перечнем операторов присваивания +=, -=, *=, /=, %= и так далее, что сделает нашу работу ещё проще. Теперь это просто как два пальца!

Грамматика для интерпретатора:

#ifndef __CALCULATOR_GRAMMAR_INTERPRETER_HPP__
#define __CALCULATOR_GRAMMAR_INTERPRETER_HPP__

#include <boost/spirit/include/qi.hpp>

namespace qi     = boost::spirit::qi;
namespace spirit = boost::spirit;

template <typename Iterator>
struct calculator_interpreter
    : qi::grammar<Iterator, int(), qi::space_type>
    {
    calculator_interpreter()
        : calculator_interpreter::base_type(expr)
        {
        expr =
            term                    [ qi::_val =    qi::_1 ]
            >> *(   '+' >> term     [ qi::_val +=   qi::_1 ]
                |   '-' >> term     [ qi::_val -=   qi::_1 ]
                )
            ;

        term =
            factor                  [ qi::_val =    qi::_1 ]
            >> *(   '*' >> factor   [ qi::_val *=   qi::_1 ]
                |   '/' >> factor   [ qi::_val /=   qi::_1 ]
                )
            ;

        factor =
                qi::uint_           [ qi::_val =    qi::_1 ]
            |   '(' >> expr         [ qi::_val =    qi::_1 ] >> ')'
            |   '-' >> factor       [ qi::_val =   -qi::_1 ]
            |   '+' >> factor       [ qi::_val =    qi::_1 ]
            ;

        }

    qi::rule<Iterator, int(), qi::space_type>
           expr, term, factor;
    };

#endif

Написание семантических правил не составило никакой трудности, коль скоро мы понимаем, что qi::_val — это то число, что получится в результате данной операции, а qi::_1 — аргумент для этой операции, над которым мы выполняем необходимое арифметическое действие. В качестве парсера пробелов мы оставляем стандартный qi::space_type парсер.

Ну и совсем простой код для тестирования данной грамматики, почти ничем не отличающийся от того, что мы писали ещё в первой части:

#include <iostream>
#include <string>

#include <boost/spirit/include/qi.hpp>

#include "calculator_grammar_interpreter.hpp"

int main()
    {
    std::cout << "//////////////////////////////////////////////\n\n";
    std::cout << "Expression parser...\n";
    std::cout << "//////////////////////////////////////////////\n\n";
    std::cout << "Type an expression... or [q or Q] to quit\n\n";

    std::string expression;

    calculator_interpreter<std::string::iterator> calc;

    while(true)
        {
        std::getline(std::cin, expression);
        if(expression == "q" || expression == "Q") break;
        std::string::iterator  begin = expression.begin()
                             , end   = expression.end();

        int result;
        bool success = qi::phrase_parse( begin
                                       , end
                                       , calc
                                       , qi::space
                                       , result);

        std::cout << "-----------------------\n";
        if(success && begin == end)
            {
            std::cout << "Parsing succeeded\n";
            std::cout << "result = " << result << "\n";
            }
        else
            {
            std::cout << "Parsing failed\nstopped at: \""
                      << std::string(begin,end) << "\"\n";
            }
        std::cout << "-----------------------\n";
        }
    }

Отлично! Вот и считать научились, очень похвально. Но опять же: вряд ли обычным калькулятором кого-то возможно в наше время удивить, да и сделать много полезного тоже сложно, ибо есть множество профессиональных и не очень инструментов, которые запросто заткнут нашу игрушку за пояс, так что не будем особо радоваться по этому поводу, а посмотрим, что можно сделать дальше.

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

Вот созданием такого калькулятора-компилятора мы и займёмся в следующий раз.

Заодно наконец познакомимся с тем, как можно делать в семантических действиях больше полезного и интересного с меньшими физическими и психическими затратами на примере инструментария, что предоставляет нам изрядно уже прожужжавший все уши boost::phoenix.

7 Comments

  1. Ответить
    Andrey 26.09.2011

    Для успешной компиляции примера из этой части необходимо добавить phoenix
    #include
    или
    #include

    Андрей.

  2. Ответить
    Andrey 26.09.2011

    #include

  3. Ответить
    QIvan 08.04.2012

    ПОечему-то не отображается что нужно подключить:
    boost/spirit/include/phoenix_operator.hpp

  4. Ответить
    nik 29.04.2013

    Не собирается пример key_value_grammar с комментариями:
    C:qBittorrentboost_1_51_0boostspirithomeqinonterminalrule.hpp||In instantiation of ‘static void boost::spirit::qi::rule::define(boost::spirit::qi::rule&, const Expr&, mpl_::false_) [with Auto = mpl_::bool_; Expr = boost::proto::exprns_::expr<boost::proto::tagns_::tag::bitwise_or, boost::proto::argsns_::list2<const boost::proto::exprns_::expr<boost::proto::tagns_::tag::terminal, boost::proto::argsns_::term<boost::spirit::tag::char_code >, 0l>&, |
    C:qBittorrentboost_1_51_0boostspirithomeqinonterminalrule.hpp|220|required from ‘boost::spirit::qi::rule& boost::spirit::qi::rule::operator=(const Expr&) [with Expr = boost::proto::exprns_::expr<boost::proto::tagns_::tag::bitwise_or, boost::proto::argsns_::list2<const boost::proto::exprns_::expr<boost::proto::tagns_::tag::terminal, boost::proto::argsns_::term<boost::spirit::tag::char_code >, 0l>&, const boost::proto::exprns_::expr<boost::proto::tagns_::t|
    E:ProgrammingExamples_13InterpreterCppkey_value_grammar.hpp|46|required from 'skip_grammar::skip_grammar() [with Iterator = __gnu_cxx::__normal_iterator<char*, std::basic_string >]’|
    E:ProgrammingExamples_13InterpreterCppMain.cpp|19|required from here|
    C:qBittorrentboost_1_51_0boostspirithomeqinonterminalrule.hpp|176|error: no matching function for call to ‘assertion_failed(mpl_::failed************ (boost::spirit::qi::rule::define(boost::spirit::qi::rule&, const Expr&, mpl_::false_) [with Auto = mpl_::bool_; Expr = boost::proto::exprns_::expr<boost::proto::tagns_::tag::bitwise_or, boost::proto::argsns_::list2<const boost::proto::exprns_::expr<boost::proto::tagns_::tag::terminal, boost::proto::argsns_::term<boost::spirit::tag::char_code<boost::spirit::tag::space|
    C:qBittorrentboost_1_51_0boostspirithomeqinonterminalrule.hpp|176|note: candidate is:|
    C:qBittorrentboost_1_51_0boostmplassert.hpp|79|note: template int mpl_::assertion_failed(typename mpl_::assert::type)|
    C:qBittorrentboost_1_51_0boostmplassert.hpp|79|note: template argument deduction/substitution failed:|
    C:qBittorrentboost_1_51_0boostspirithomeqinonterminalrule.hpp|176|note: cannot convert ‘boost::spirit::qi::rule::define(boost::spirit::qi::rule&, const Expr&, mpl_::false_)::error_invalid_expression176::assert_arg<mpl_::bool_, boost::proto::exprns_::expr<boost::proto::tagns_::tag::bitwise_or, boost::proto::argsns_::list2<const boost::proto::exprns_::expr<boost::proto::tagns_::tag::terminal, boost::proto::argsns_::term<boost::spirit::tag::char_code<boost::spirit::tag::space, boost::spirit::char_encoding::standar|
    ||=== Сборка закончена: 1 errors, 4 warnings (0 minutes, 11 seconds) ===|

    Полный лог:
    http://pastebin.com/isBhVjcg

  5. Ответить
    nik 29.04.2013

    проекты:
    CodeBlocks
    Eclipse
    Netbeans
    Visual Studio 2010
    http://yadi.sk/d/OkmOGq8m4Qm22

  6. Ответить
    Suares 03.01.2015

    Идеальное изложение материала, спасибо:)

  7. Ответить
    reaper 05.08.2017

    поддерживаю Суарес, вторая часть смачная, спасибо!

Добавить комментарий для nik Отменить ответ

Ваш адрес email не будет опубликован.