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

BOOST_FUSION_ADAPT_STRUCT — автоматическое распространение атрибутов для пользовательских типов.

В предыдущих частях (1-я, 2-я, 3-я, 4-я) я говорил об автоматическом заполнении STL структур атрибутами при парсинге. Таким образом мы, например, заполняли список пар «ключ=значение», пользуясь тем, что сами пары могли автоматически конвертироваться в std::pair<std::string, std::string>, а список пар спокойно мог заполнить собой std::vector<std::pair<std::string, std::string> >. Весь этот механизм, как тоже было сказано ранее, основывается на том, что для стандартных контейнеров STL существуют специальные обёртки, благодаря которым они могут считаться полноценными boost::fusion последовательностями.

Как быть, если мы хотим, чтобы наша собственная структура при парсинге могла бы автоматически заполняться так же легко, как это например делают std::vector или std::string? Оказывается, способ есть. Притом достаточно простой. Я не зря сказал про обёртки, которыми снабжены стандартные контейнеры библиотеки STL. В нашем случае придётся сделать в точности то же самое — снабдить нашу структуру всем необходимым в этом плане, иными словами: сказать Boost.Fusion, что наша структура является полноправным Fusion контейнером, построенном на модели random access sequence. Специально для этого в Fusion есть макрос BOOST_FUSION_ADAPT_STRUCT, который позволяет адаптировать любую пользовательскую структуру в полноправный Fusion контейнер.

Выглядит он примерно следующим образом:

BOOST_FUSION_ADAPT_STRUCT(
    struct_name,
    (member_type0, member_name0)
    (member_type1, member_name1)
    ...
    )

Макрос должен использоваться в глобальном пространстве имён. Теперь о том, что здесь к чему: struct_name — полностью квалифицированное имя структуры (со всеми пространствами имён), которую хотим адаптировать. Пары (member_typeN, member_nameN) определяют типы и имена каждого члена структуры, которые станут частью адаптированной последовательности.

Пример:

namespace Example
    {
    struct ExampleStruct
        {
        int IntField;
        std::string StringField;
        };
    }

BOOST_FUSION_ADAPT_STRUCT
    (
    Example::ExampleStruct,
    (int, IntField)
    (std::string, StringField)
    )

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


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

struct Employee
    {
    int Age;
    std::string Surname;
    std::string Forename;
    double Salary;
    };

Теперь делаем из этой структуры Fusion контейнер с помощью макроса BOOST_FUSION_ADAPT_STRUCT:

BOOST_FUSION_ADAPT_STRUCT
    (
    Employee,
    (int, Age)
    (std::string, Surname)
    (std::string, Forename)
    (double, Salary)
    )

Теперь нужно подумать о том, в каком виде записи о рабочих надо парсить. Пускай будет примерно так: employee{ age, "surname", "forename", salary }. Пишем соответствующий парсер:

template 
struct EmployeeParser
    : qi::grammar
    {
    EmployeeParser()
        : EmployeeParser::base_type(employee_rule)
        {
        quoted_string = qi::lexeme['"' >> +(qi::char_ - '"') >> '"'];

        employee_rule =
                qi::lit("employee")
            >>  '{'
            >>  qi::int_ >> ','
            >>  quoted_string >> ','
            >>  quoted_string >> ','
            >>  qi::double_
            >> '}'
            ;
        }

    qi::rule quoted_string;
    qi::rule employee_rule;
    };

Реальный тип атрибута от выражения правой части правила employee_rule является следующим:

fusion::vector

Поскольку мы адаптировали структуру Employee, чтобы она была нормальной Fusion последовательностью, то приведённый выше тип fusion::vector<...> может быть сконвертирован в тип структуры Employee точно так же, как это могут делать стандартные контейнеры STL, типа std::pair, std::map или std::vector, что позволяет полагаться на автоматическое распространение атрибутов без необходимости писать семантические действия в правиле, направленные на заполнение необходимых полей данной структуры.

Удобная работа с ключевыми словами — директива kwd[].

В Спирите есть очень удобный механизм для распознавания входных данных, основанных на ключевых словах — директивы kwd[] и ikwd[]. Для того, чтобы работать с ними, должен быть подключён заголовок


Находится директива в пространстве имён boost::spirit::repository::qi. Синтаксис директивы kwd[] может быть одним из следующих:

kwd(keyword)[subject]
kwd(keyword, n)[subject]
kwd(keyword, min, max)[subject]
kwd(keyword, min, inf)[subject]

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

  1. kwd(keyword)[subject]
    Этот вариант парсит такие строки: ("keyword" > subject) нуль и более раз.
  2. kwd(keyword, n)[subject]
    Такая штука парсит ("keyword" > subject) ровно n раз.
  3. kwd(keyword, min, max)[subject]
    Парсинг выражения ("keyword" > subject) от min до max раз.
  4. kwd(keyword, min, inf)[subject]
    Обрабатывает выражение ("keyword" > subject) как минимум min раз.

В начале я упомянул про директиву ikwd[] — она отличается от kwd[] только тем, что ключевые слова будут распознаваться без учёта регистра символов. Смешивать вместе kwd[] и ikwd[] можно, но всё-таки не рекомендуется, если что.

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

///////////////////////////////////////
// kwd(k1)[a]
a: A --> kwd(k1)[a]: optional or vector a: Unused --> kwd(k1)[a]: Unused /////////////////////////////////////// // kwd(k1,n)[a] a: A --> kwd(k1,n)[a]: optional or vector a: Unused --> kwd(k1,n)[a]: Unused /////////////////////////////////////// // kwd(k1,min, max)[a] a: A --> kwd(k1,min, max)[a]: optional or vector a: Unused --> kwd(k1,min, max)[a]: Unused /////////////////////////////////////// // kwd(k1,min, inf)[a] a: A --> kwd(k1,min, inf)[a]: optional or vector a: Unused --> kwd(k1,min, inf)[a]: Unused

Как быть, если нужно парсить связку из данных, разделённых разными ключевыми словами?
Для этого есть специальный оператор '/', применяемый с директивами kwd[] и ikwd[]. Например, запись kwd("k1")[a] / kwd("k2")[b] будет эквивалентна следующему коду:

*("k1" > a | "k2" > b)

То бишь, этот оператор очень схож с оператором выбора альтернативы в некотором смысле. Как только распознано одно из ключевых слов, входящих в связку, сразу же будет сделана попытка применить парсер subject, стоящего за данным ключевым словом. Можно, конечно, и проигнорировать существование данной директивы и писать напрямую через оператор альтернативы, но тогда мы потеряем в скорости, ибо при связке нескольких директив с помощью оператора '/' внутри этого всего дела конструируется троичное дерево поиска для эффективного парсинга ключевых слов, да и удобней так, в конце концов, чего писать велосипеды, когда есть такая фича уже «из коробки».

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

struct Person
    {
    std::string name;
    unsigned age;
    std::vector favourite_colours;
    };

BOOST_FUSION_ADAPT_STRUCT
    (
    person,
    (std::string, name)
    (unsigned, age)
    (std::vector, favourite_colours)
    )

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

На основе этих данных пишем правило для заполнения структуры Person:

person %=
      kwd("name", 1)         [ '=' > string_rule ]
    / kwd("age" , 1)         [ '=' > int_        ]
    / kwd("favourite colour")[ '=' > string_rule ]
    ;

string_rule здесь выступает в качестве некоторого правила, распознающего строки, например, заключённые в кавычки — так удобней.

Теперь мы имеем возможность парсить строки примерно следующего вида прямо в структуру Person:

"name = \"John\" \\n age = 10 "
"age = 10 \\n name = \"Mary\""
"name = \"Hellen\" \\n age = 10 \\n favorite colour = \"blue\" \\n favorite colour = \"green\" "

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

Между тем, следующая запись провалит парсинг, потому что в ней не соблюдены условия на количество появлений ключевого слова "age":

"name = \"Johny\"  \\n favorite colour = \"blue\" \\n favorite colour = \"green\" "

Таблицы символов qi::symbols.

Класс symbols представляет из себя ассоциативный контейнер пар ключ-значение, где ключи представляются в виде строк, а тип значения выбирается в соответствии со вторым шаблонным параметром. Этот класс может нормально работать со строками 8, 16, 32, 64-битного символьного типа. Класс symbols является примером «динамического» парсера — то есть парсера, поведение которого может быть изменено в ходе выполнения программы. Чтобы пользоваться этим парсером, нужно подключить следующий заголовок:


Заголовок этого шаблонного класса выглядит следующим образом:

template 
struct symbols;

Char описывает тот символьный тип, который будет использоваться для ключевых строк в таблице символов. T — тип данных, ассоциированный с каждой записью в таблице, Lookup отвечает за тип внутреннего представления структуры поиска ключей в таблице.

Интерфейс для добавления, удаления или присвоения записей в таблицы qi::symbols в некотором смысле схож с тем, как реализованы подобные вещи в библиотеке boost::assign. Так, для присвоения таблице некоторого набора (s1, s2, ..., sN) символов можно воспользоваться нотацией

sym = s1, s2, s3, ..., sN;

Для добавления символов в таблицу можно воспользоваться одним из следующих вариантов:

// add symbols to qi::symbols instance via operator+=
sym += s1, s2, s3, ..., sN;
// add symbols via 'add' adapter
sym.add(s1)(s2)...(sN);
// add symbols (s1, ..., sN) with associated data (d1, ..., dN)
sym.add(s1, d1)(s2, d2)...(sN, dN);

Заметьте, в случае использования add мы имеем возможность как указывать каждой записи ассоциированное значение, так и не указывать его.

Для удаления символов из таблицы используются похожие функции, называются operator-= и remove соответственно:

sym -= s1, ..., sN;
sym.remove(s1)(s2)...(sN);

Для очистки таблицы есть функция clear, используется так же, как и в STL контейнерах.

Внутреннее представление данных может быть различно в зависимости от того, какой вариант реализации выбираем, а их на данный момент две штуки: либо всё те же троичные деревья поиска (TST), что и в реализации директив kwd[], либо гибридный вариант из hash-map для первого символа плюс TST, что иногда бывает существенно быстрее, нежели классическая реализация только на TST, только по затратам памяти несколько больше выходит. Настроиться на использование второго варианта можно, указав в качестве третьего шаблонного параметра для класса symbols тип tst_map:

symbols > sym;

Небольшой примерчик:

qi::symbols sym;

sym.add("fuck", 0)
    ("suck", 1)
    ("shit", 2)
    ("bullshit", 3)
    ;

int myChoice;
test_parser_attr("fuck", sym, myChoice);
std::cout << myChoice << std::endl; // prints "0"

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

Для этого создадим несколько таблиц для представления единиц, десятков и сотен:

struct ones_ : qi::symbols
    {
    ones_()
        {
        add
            ( "I"   , 1 )
            ( "II"  , 2 )
            ( "III" , 3 )
            ( "IV"  , 4 )
            ( "V"   , 5 )
            ( "VI"  , 6 )
            ( "VII" , 7 )
            ( "VIII", 8 )
            ( "IX"  , 9 )
            ;
        }
    } ones;

struct tens_ : qi::symbols
    {
    tens_()
        {
        add
            ( "X"   , 10 )
            ( "XX"  , 20 )
            ( "XXX" , 30 )
            ( "XL"  , 40 )
            ( "L"   , 50 )
            ( "LX"  , 60 )
            ( "LXX" , 70 )
            ( "LXXX", 80 )
            ( "XC"  , 90 )
            ;
        }
    } tens;

struct hundreds_ : qi::symbols
    {
    hundreds_()
        {
        add
            ( "C"   , 100 )
            ( "CC"  , 200 )
            ( "CCC" , 300 )
            ( "CD"  , 400 )
            ( "D"   , 500 )
            ( "DC"  , 600 )
            ( "DCC" , 700 )
            ( "DCCC", 800 )
            ( "CM"  , 900 )
        }
    } hundreds;

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

template 
struct roman
    : qi::grammar
    {
    roman()
        : roman::base_type(StartRule)
        {
        using qi::eps;
        using qi::lit;
        using qi::_val;
        using qi::_1;
        using ascii::char_;

        StartRule = eps         [_val = 0] >>
            (
                +lit('M')       [_val += 1000]
                ||  hundreds    [_val += _1]
                ||  tens        [_val += _1]
                ||  ones        [_val += _1]
            )
            ;
        }

    qi::rule StartRule;
    };

По ходу дела встретились ещё с одним оператором парсинга в Спирите: секвенциальный условный оператор '||'. В выражении a || b этот оператор значит следующее: распознавать либо a, либо b в последовательности. То есть если и a, и b будут распознаны, то они должны идти по порядку. Возможно, объяснение не совсем хорошее, так что если формально, то действие этого оператора будет эквивалентно следующему выражению, только несколько более эффективно:

a >> -b | b

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

Директива distinct[].

Директива distinct[] предназначена для того, чтобы избежать неполных случаев распознавания строк при использовании пропускателей (парсеров пропусков). Обычно применяется для того, чтобы безопасным образом отделять ключевые слова во входных данных. Рассмотрим следующий пример: допустим, нам нужно распознавать строчки вида data[':']<some_data>, то есть слово data после которого может либо стоять двоеточие, либо не стоять, а дальше необходимые данные до конца строки. Как можно решить данную задачу:

"data" >> -lit(':') >> *(char_ - eol)

Вроде ништяк, но на самом деле есть проблемы: предположим, мы используем стандартный пропускатель space, тогда следующие три варианта одинаково успешно пройдут процедуру распознания:

"data: some_data\\n"
"data some_data\\n"
"datasome_data\\n"

Немного неожиданно, конечно, но так и будет, а всё из-за того, что в виду возможности отсутствия двоеточия после data вступает в дело парсер *(char_ - eol), который успешно съедает все символы в строке, потому как пропускатель ничего не может с этим поделать, даже если необходимых пробелов нет.

Чуть-чуть подумав, приходим к примерно такому решению проблемы:

lexeme["data" >> !(alnum | '_')] >> -lit(":") >> *(char_ - eol)

Оператор '!' (называется обычно «not-предикат») предназначен для проверок хода парсинга, делает примерно следующее: если в выражении !p парсер p завершает работу успешно, то парсинг проваливается, и наоборот. Обратный вариант этого оператора — оператор '&' («and-предикат») — завершается успехом только тогда, когда подконтрольный парсер отрабатывает успешно. Отличительная черта этих парсеров в том, что они, точно так же, как и парсер eps, не поглощают входных данных, возвращая результат считывания нулевой длины. Атрибут подконтрольных парсеров тоже игнорируется, то есть, если a имеет атрибут типа A, то парсер !a имеет атрибут типа unused_type, то бишь он просто не используется.

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

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

distinct(alnum | '_')["data"] >> -lit(":") >> *(char_ - eol)

Кроме того, что выглядит чуть попроще, как обычно это и бывает с директивами, также появляется потенциал для более удобного повторного использования и инкапсуляции хвостового парсера для разделения (в нашем случае это alnum | '_') в отдельную конструкцию.

В официальной документации даже можно найти примерчик того, как приведённый пример можно ещё больше упростить за счет объявления синонима для директивы distinct(alnum | '_'), но это уже включает более глубокий взгляд во внутренности того, как работают такие механизмы внутри самой библиотеки, так что об этом я здесь говорить не буду, кому надо, тот почитает в документации с необходимыми пояснительными моментами.

Находится эта директива в пространстве имён boost::spirit::repository::qi, а для её использования нужно подключить следующий заголовок:


Трансдукционный парсинг — директива raw[].

Всё, что делает директива raw[] — отвергает атрибут подконтрольного парсера, взамен предоставляя в качестве возвращаемого атрибута полуоткрытый интервал [first, last), состоящий из считанных с входного потока символов.
Для использования подключаем следующий заголовок:


Сама директива находится в пространстве имён boost::spirit, но для неё также существует синоним в пространстве имён boost::spirit::qi.

Указанный интервал символов, возвращаемый директивой, является объектом типа boost::iterator_range<Iterator>, который держит пару итераторов на начало и конец произвольного интервала с заданным типом итератора. Правила распространения атрибута следующие:

///////////////////////////////
// raw[a] directive
a: A --> raw[a]: boost::iterator_range
a: Unused --> raw[a]: Unused

Данная директива имеет только одно основное применение, основанное на соображениях производительности: например, распознавая строку в парсере с атрибутом типа std::string, при успешном парсинге будет создан объект std::string с заданной строкой, а потом он будет ещё раз скопирован уже в ту строку, что мы, например, ожидаем при выходе из фунции parse. Применяя директиву raw[], удаётся избежать ненужного копирования и создания объектов, которые в Boost.Spirit происходят очень-очень часто и с чём люди борются различными способами.

Типичный пример использования:

std::string input = "fucking_shit";
std::string output;
parse(input.begin(), input.end(), raw[*(alpha | '_')], output);
std::cout << output << std::endl; // prints fucking_shit

Частичное применение функций и phoenix::bind.

В третьей части я рассказывал о том, как можно адаптировать обычные функции и функциональные объекты для исполнения в ленивой манере. Для этого мы пользовались услугами phoenix::function — класса, имитирующего ленивые функции. Но ленивые функции не отходя от кассы можно получать и несколько другим способом. В той же третьей части я упоминал термин «частичное применение функций», именно о нём и пойдёт речь. Это явление также принято называть связыванием (binding) — операция привязывания некоторых аргументов функции для последующего отложенного выполнения. В общем случае phoenix::bind, равно как и boost::bind, является обобщением стандартных функций std::bind1st и std::bind2nd, которые позволяли в качестве первого или второго аргумента некоторой функции привязать некоторое значение, получая на выходе уже унарный функтор, где вместо одного из аргументов находится привязанное значение, который можно удобно передавать в стандартные алгоритмы.

Впрочем, для тех, кто по каким бы то ни было причинам не пользовался указанными функторами std::bind1st или std::bind2nd, или нигде (хотя бы и в другом языке) не сталкивался вообще с понятием частичного применения функций, мои объяснения вряд ли внесут особую ясность, так что сейчас пойдут наглядные примеры, описывающие суть данного процесса.

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

Например, пусть у нас есть следующая функция:

void func(int num)
    {
    std::cout << num;
    }

Тогда её можно привязать для отложенного выполнения следующим образом:

phoenix::bind(&f, phoenix::placeholders::_1)

Получившийся в результате этой операции объект является ленивым унарным функтором, который может быть использован аналогично ленивым функциям phoenix::function, например, так:

// prints 10
bind(&f, _1)(10);

Также можно привязывать различным аргументам функции конкретные значения. Например, пусть даны функции

int foo(int a, int b)
    {
    return a * b;
    }

int bar(int a, int b, int c)
    {
    return a * b + c;
    }

Тогда мы можем привязать функции foo в качестве первого аргумента, например, значение 1: bind(&foo, 1, _1) — результатом будет унарная ленивая функция, в качестве первого аргумента которой будет 1. Вызов bind(&foo, 1, _1)(2) будет эквивалентен вызову функции foo(1, 2), что вернёт значение 2. Приведённый только что пример с использованием стандартного связывателя std::bind1st выглядел бы так: std::bind1st(std::ptr_fun(foo), 1)(2). Точно так же, выражение bind(&foo, _1, 1)(2) привязывает второму аргументу фиксированное значение, что равносильно применению связывателя («binder» — если по-буржуйски) std::bind2nd, вот то же действие с его использованием: std::bind2nd(std::ptr_fun(foo), 1)(2). Вариант с phoenix::bind, равно как и boost::bind или std::bind, расширяет функциональность этих двух стандартных компонент связывания, добавляя возможность привязывать функции с произвольным числом аргументов.

С привязыванием аргументам фиксированного значения вроде бы всё достаточно просто, теперь чуть-чуть примеров, связанных с применением имён-заместителей phoenix:

bind(foo, _1, _1)(x, y); // foo(x, x)
bind(foo, _2, _2)(x, y); // foo(y, y)
bind(foo, _1, _2)(x, y); // foo(x, y)
bind(foo, _2, _1)(x, y); // foo(y, x)
bind(bar, _1, _1, _1)(x, y, z); // bar(x, x, x)

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

Первое, что приходит в голову, при желании реализовать приведённую схему — просто вложить вызовы bind один в другой, наподобие следующего:

bind(bind(bind(bar, 2, _1, _2), 4, _1), 5)

Здесь мы ожидаем, что на выходе получится нульарная функция типа bar(2, 4, 5), но такой подход не работает, потому как для того, чтобы каждый вызов bind с соответствующими аргументами-заместителями работал нормально, нужно, чтобы для каждого из них эти подстановочные аргументы _1 и _2 представлялись в своём определённом контексте. Иными словами, тот _1, что находится в самом глубоком вызове bind должен иметь иной смысл, чем аргумент _1 в более внешнем вызове bind. На деле этого не происходит и все применения имён-заместителей здесь соотносятся в одну кучу с одинаковым контекстом. В итоге, из-за того, что для каждого последующего вызова bind не создаётся новое окружение и область видимости, применяемая к подстановочным аргументам, чтобы они могли адекватно менять своё смысловое значение в каждом связывании, получаем громадное количество ошибок компиляции, так как семантика применения заместителей _1, _2 оказывается неверной. Вот если бы существовал способ для каждого связывания определить своё окружение, в рамках которого заместители работали бы как надо… Впрочем, такой способ существует.

Я уже упоминал, что в Phoenix существуют механизмы для контроля областей видимости и создания локальных переменных, сейчас самое время поговорить об этом. А чуть позже вернёмся к этому примеру последовательного связывания аргументов.

Локальные переменные в Phoenix.

Для обозначения локальных переменных в Phoenix используются объекты типа

expression::local_variable::type

В данном случае, Key представляет собой произвольный тип для имитируемой локальной переменной. В Phoenix существует набор предопределённых локальных переменных типа expression::local_variable<Key>::type с именами от _a до _z. Все они находятся в следующем пространстве имён:

namespace boost::phoenix::local_names

Для работы с локальными переменными в Phoenix нужно подключить заголовок


Объявление локальных переменных — блоки let.

Для того, чтобы объявлять локальные переменные, в Phoenix существует конструкция под названием let с использованием следующего синтаксиса:

let(local_declarations)
    [
    let_body
    ]

В шапке local_declarations для let возможно разместить от 1 до N объявлений локальных переменных через запятую (N == BOOST_PHOENIX_LOCAL_LIMIT). Каждое из объявлений представляется в следующем виде:

local_id = lambda_expression

let_body представляет из себя последовательность ленивых statement’ов, разделённых между собой запятыми, точно так же, как это происходит в теле ленивых аналогов для if, for, while, do_while.

Для того, чтобы пользоваться, подключаем следующий заголовок:


Простой пример использования:

let(_a = 1, _b = 2)
    [
    std::cout << _a + _b << std::endl
    ]

То есть ситуация примерно следующая: в шапке конструкции let объявляем переменные вместе с их начальными значениями, а в теле, заключённом в квадратные скобки, действия, в пределах которых данная переменная будет находиться в области видимости, чем-то похоже на using statement в C# в том плане, что в шапке тоже происходит инициализация переменной и за пределами фигурных скобок, ограничивающих using выражение, переменная выходит из области видимости.

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

let(_a = _1, _b = 0) ...

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

int var = 0;

let(_a = _1)
    [
    std::cout << ++_a << std::endl
    ](var)
    ;

std::cout << var;

Выведется два раза значение 1, потому что захват переменной var через аргумент-заместитель происходит по ссылке. В то же время, следующий код напечатает 1, а потом 0, потому как val является rvalue, и аргумент передаётся по значению:

int var = 0;

let(_a = val(_1))
    [
    std::cout << ++_a << std::endl
    ](var)
    ;

std::cout << var << std::endl;

Таким образом, те штуки, которые считаются lvalue (а к ним относятся заместители типа _N и ссылки ref), передаются по ссылке, а те, что являются rvalue (то есть непосредственные значения и val, например), передаются по значению.

Область видимости переменных.

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

Небольшой примерчик:

let(_a = "Fuck you", _b = ", World!")
    [
    let(_a = "Hello")
        [
        // prints "Hello, World!"
        std::cout << _a << _b << std::endl
        ]
    ]

По поводу присваиваний в заголовке let имеется один существенный момент: правая часть каждого локального объявления в заголовке let не может использовать ни один из идентификаторов локальных переменных, встречающихся в левых частях данного заголовка блока let. То есть следующий пример не прокатит, потому как переменная _a на момент объявления в заголовке let ещё не числится в зоне видимости:

// error - _a is not in scope yet!
let(_a = 1, b = _a)
    [
    ]

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

let(_a = "Hell yeah, motherfucker!")
    [
    let(_a = 0, _b = _a)
        [
        // now _a is int = 0
        // _b refers to the outer _a
        std::cout << _b // prints "Hell yeah, motherfucker!"
        ]
    ]

phoenix::lambda — создание новых областей видимости.

Рассмотрим типичный пример с передачей функторов в алгоритмы. В качестве алгоритма возьмём реализацию «ленивого» for_each, которую я приводил ранее, если кто забыл, выглядело это примерно следующим образом:

struct for_each_impl
    {
    template 
    struct result
        {
        typedef void type;
        };

    template 
    void operator() (Container & c, Function f) const
        {
        std::for_each(c.begin(), c.end(), f);
        }
    };

phx::function const for_each = for_each_impl();

В чём здесь соль: operator() в качестве аргумента принимает произвольную функцию f, которая уже передаётся стандартному std::for_each; область видимости данной функции f ограничена рамками operator(), в то же время, уже внутри функции std::for_each эта функция будет находиться в новой области видимости вместе с новыми аргументами. При этом та новая область видимости никак не будет связана с более внешними областями видимости далее, чем область operator().

Собственно, переходим к тому, что только что сказанное представляет из себя на практике. Предположим, что с помощью нашего for_each нужно вывести все элементы контейнера в cout, при этом использовать phoenix-лямбду в качестве функтора, осуществляющего вывод. Наверняка, сразу же захочется написать что-то вроде этого:

for_each(_1, std::cout << _1)

Несмотря на то, что мы вроде бы понимаем, что _1 в качестве первого аргумента for_each является заместителем для контейнера, который будет передаваться, а _1 в выражении с std::cout отвечает за конкретный элемент в контейнере, на деле получается немного не то. А что именно получается: поскольку оба заместителя _1 находятся в «одной» области видимости, то они имеют одинаковую семантику, то есть, запись

std::vector vec{1, 2, 3};
for_each(_1, std::cout << _1 << ' ')(vec);

по сути будет означать следующее: в качестве _1 для первого аргумента передать контейнер vec, и в качестве _1 для второго аргумента тоже передать контейнер vec, хотя нам бы хотелось, чтобы _1 в части с std::cout относился бы к тому, что будет передаваться этой лямбде уже внутри вызова std::for_each, который запрятан внутри нашего рукописного for_each. Так вот для того, чтобы для этого заместителя в лямбде предоставить новую область видимости и чтобы он уже указывал на то, что будет передаваться непосредственно лямбде, приобрёл новое значение, нужно воспользоваться такой фигнёй под названием phoenix::lambda.

Чтобы использовать lambda нужно подключить заголовок


Синтаксис lambda блока может быть одним из следующих:

lambda
    [
    lambda_body
    ]
lambda(local_declarations)
    [
    lambda_body
    ]

В первом варианте новых локальных переменных не объявляем, а во втором случае объявляем, точно так же, как и у let: предел на количество объявлений переменных равен BOOST_PHOENIX_LOCAL_LIMIT. Точно те же, как и в случае с let, накладывается ограничение на применение и область видимости локальных переменных: в правых частях объявлений в заголовке lambda не может находиться ни одного идентификатора локальной переменной, который появляется тут же в левых частях объявлений, то есть следующая запись будет ошибочной по причине того, что переменная _a ещё не находится в области видимости объявляемой переменной _b:

lambda(_a = 1, _b = _a) // error - _a not in scope yet!
    [
    // ...
    ]

Как будет выглядеть правильная реализация недавнего примера с применением данной фичи:

std::vector vec{1, 2, 3};
for_each(_1, lambda[std::cout << _1 << ' '])(vec);

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

Также имеется возможность захвата аргументов из внешних областей видимости, чего, например, блок let делать не умеет. Рассмотрим маленький примерчик на эту тему. Пусть дан двумерный массив чисел (например, представленный в виде std::vector<std::vector<int> >) и некторое фиксированное число. Требуется запихнуть в каждый одномерный подмассив в конец это число. Для этого нам может потребоваться ленивый вариант функции контейнера push_back, пример которого я также приводил ранее, если что, напомню его вид:

struct push_back_impl
    {
    template 
    struct result
        {
        typedef void type;
        };

    template 
    void operator() (Container & c, Arg & arg) const
        {
        c.push_back(arg);
        }
    };

phx::function
 const push_back = push_back_impl();

Теперь решение данной задачи с помощью наших ленивых for_each, push_back, lambda и вышеупомянутой фичи с захватом внешних аргументов:

for_each(_1, lambda(_a = _2)
                [
                push_back(_1, _a)
                ]
        )

Последовательное связывание с помощью phoenix::bind.

Теперь мы понимаем, как сделать так, чтобы подстановочные аргументы приобретали в зависимости от контекста нужный смысл — нужно оборачивать каждый последующий вызов phoenix::bind в тело блока lambda, чтобы предоставить новую область видимости для подстановочных аргументов и тем самым добиться желаемого результата. Итого, выход будет выглядеть так:

std::cout <<
    bind(lambda[
            bind(lambda[
                    bind(bar, 2, _1, _2)
                ], 4, _1)
        ], 5)() << std::endl;
// prints "13"

Что тут происходит: сначала мы привязываем первый аргумент функции bar со значением 2, потом привязываем второй аргумент со значением 4, а в конце привязываем оставшийся аргумент со значением 5 и вызываем получившуюся нульарную функцию, которая вычисляет выражение 2 * 4 + 5 == 13.

Привязывание функторов, членов-функций и переменных-полей классов.

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

Кстати, для того, чтобы привязывать функции, должен быть подключён заголовочный файл


А для того, чтобы привязывать функциональные объекты, такой:


Синтаксис привязывания в точности такой же, как и в случае с функциями:

struct func
    {
    int operator() (int a, int b)
        {
        return a + b;
        }
    };

func f;

int x = bind(f, _1, _1)(2);
// x == 4

Основное применение такой фичи для функторов состоит в быстрой адаптации функтора под «ленивую» модель поведения. Впрочем, если уж делать, то по-нормальному — предпочтительным вариантом для создания «ленивых» функциональных объектов является phoenix::function, с этим способом мы уже имели возможность познакомиться парой частей ранее.

Привязывание членов-функций.

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

struct foo
    {
    void bar(int a) const
        {
        // ...
        }
    };

foo obj;

// calls obj.bar(1)
bind(&foo::bar, obj, _1)(1);

Также, лениво привязан может быть как объект, чью функцию надо вызывать, так и параметры, передающиеся функции-члену. Ещё примерчики:

foo obj;
bind(&foo::bar, _1, _2)     // arg1.bar(_2)
bind(&foo::bar, obj, _1)    // obj.bar(_1)
bind(&foo::bar, obj, 100)   // obj.bar(100)

Для того, чтобы пользоваться, подключаем следующий заголовок:


Привязывание переменных-полей класса.

Тут сразу же подключаем следующее:


Здесь всё выглядит аналогично тому, как мы привязывали функции-члены классов. Точно так же, для доступа к полю класса требуется ссылка или указатель на объект, чьё поле хотим привязать. В качестве возвращаемого значения выступает ссылка на привязанную переменную. Пример использования:

struct foo
    {
    bool flag;
    };

foo obj;

bind(&foo::flag, obj) // obj.flag is bound

Лениво привязанная переменная-член ожидает ровно один аргумент, который должен быть ссылкой или указателем на объект. Сам объект также может быть лениво привязан. Примеры:

foo obj;
bind(&foo::flag, _1)               // _1.flag
bind(&foo::flag, obj)              // obj.flag
bind(&foo::flag, _1)(obj) = true   // obj.flag = true

Как утверждают разработчики Phoenix, их вариант bind полностью совместим с boost::bind, boost::tr1::bind, std::bind, так что можно не бояться применять его заместо указанных решений, если возникнет необходимость (хотя было время, на багтрекере то и дело выскакивали билетики, связанные с тем, что на некоторых компиляторах некоторые тесты проваливаются; надеюсь, что уже пофиксили).

Лексический анализатор Spirit.Lex.

Ещё одной частью Boost.Spirit является Spirit.Lex — генератор лексических анализаторов, довольно удобных в использовании и обладающих притом серьёзными возможностями. В принципе, как мы уже многократно убеждались, использование отдельного лексического анализатора для Spirit.Qi абсолютно необязательно, но по мере усложнения грамматик, было бы естественно разбивать их на множество более мелких в соответствии со смыслом и выполняемыми функциями, часто повторяющиеся токены выделить в отдельные конструкции, чтобы использовать для обращения к ним только мнемонические имена, всё это, несомненно, облегчит сопровождение всей системы в целом.

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

  1. Нет дополнительной стадии компиляции. Не нужно пользоваться сторонними инструментами, которые уже с помощью различных неудобных интерфейсов нужно пристыковывать к готовому коду, всё делается непосредственно в коде C++.
  2. Возможность строить динамические лексические анализаторы, поведение которых можно изменять прямо по ходу дела.
  3. Возможность прицеплять к объявлениям токенов семантические действия в той же манере, как это делается и в Spirit.Qi, в том числе возможно использование Boost.Phoenix для составления семантических действий.
  4. Очень удобная и простая интеграция с компонентой для парсинга Spirit.Qi, да и вообще понятные и хорошие интерфейсы для работы.

Теперь о том, чем в основном приходится пользоваться в этой библиотеке и как оно примерно работает.

Большую часть времени Spirit.Lex используют как генератор динамических лексеров, что означает то, что необходимые таблицы для разбора генерируются во время выполнения из объявлений токенов, написанных, кстати, на языке регулярных выражений. Но, наряду с динамической моделью, существует также возможность использовать статическую модель, получая на выходе ДКА (на русском), что очень похоже на то, как действует генератор Flex, создающий из описания токенов, представленных на специальном языке, необходимые таблицы и функции для того, чтобы это всё добро как-то использовать. Таким образом, вопрос эффективности работы Spirit.Lex не стоит: если не устраивает то, как работает динамическая модель, можно запросто поменять динамический лексер на статический (как это сделать, можно почитать в документации) и всё будет просто замечательно.

Пространство имён библиотеки: boost::spirit::lex. Также основу многих концепций была взята реализация из библиотеки lexertl, поэтому много что находится в соответствующем пространстве имён boost::spirit::lex::lexertl.

Основным элементом лексического анализатора являются токены. В Spirit.Lex для описания токенов нужно иметь представление о двух вещах и уметь их различать: определение токена (класс lex::token_def<>) и сам класс, представляющий токен (класс lex::lexertl::token<>).

Объявления токенов служат для того, чтобы:

  • Упростить определение токена с помощью регулярных выражений для распознания данного типа токена.
  • Указать тип атрибута для значения токена.
  • Присвоить собственный идентификатор для типа токена.
  • Ассоциировать тип токена с определённым состоянием лексера.
  • Ассоциировать некоторый код с типом токена, который будет исполнен каждый раз, как будет распознан очередной токен данного типа (семантические действия, иными словами).

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

В то время, как token_def определяет свойства какого-то определённого токена, то сам token должен определять свойства, общие для всех объявлений токенов, которые входят в состав лексического анализатора, потому как именно тип токена является шаблонным параметром для типа лексера. То есть в рамках одного лексера может быть несколько определений токенов совершенно различного вида: одни могут в качестве атрибута возвращать строки std::string, другие числа типа int, третьи могут вообще игнорировать свой атрибут, а между тем класс token должен поддерживать каждое из этих определений в отдельности и, естественно, все их вместе тоже должен хорошо переваривать. Поэтому в общем случае типом атрибута токена является знакомый нам boost::variant, позволяющий содержать значение одного из перечисленных в списке шаблонных параметров типов. Естественно, в этом boost::variant типе должны быть указаны все типы атрибутов, которые появляются в объявлениях токенов, иначе получим ошибки компиляции.

Для типа токена обычно используется шаблонный класс lex::lexertl::token<>. По-умолчанию значение атрибута токена, если не указано явно, будет представлять собой интервал считанных символов boost::iterator_range, помещённый в класс boost::variant. Прототип этого шаблона выглядит примерно следующим образом:

template <
    typename Iterator = char const*,
    typename AttributeTypes = mpl::vector0<>,
    typename HasState = mpl::true_
>
struct lexertl_token;

Параметр Iterator — это тип итератора, используемый для доступа к входному потоку. AttributeTypes — либо mpl последовательность, содержащая все типы, встречающиеся в определениях токенов token_def, либо тип omit, если атрибут должен быть проигнорирован. HasState — константа, определяющая, будет ли храниться в токене информация о том, в каком состоянии лексера он был сгенерирован, изначально равен mpl::true_ — обёртка для булевого значения true из библиотеки boost::mpl, что означает то, что все токены по-умолчанию содержат в себе состояние лексера. Обычно, в процессе конструирования, объект токена содержит в себе значение типа boost::iterator_range, только если не был указан тип omit в качестве типа атрибута.

С самими токенами вроде бы разобрались, теперь чуть-чуть об определениях токенов token_def. Прототип класса lex::token_def выглядит следующим образом:

template<
    typename Attribute = unused_type,
    typename Char = char
>
class token_def;

Точно так же: Attribute отвечает за тип атрибута, который будет порождён при генерации токена, а Char — за тип значения для итераторов во входной поток символов. Как и для самих токенов, если не указан явно тип атрибута, по-умолчанию атрибутом будет считаться boost::iterator_range, представляющий собой пару итераторов, указывающих на диапазон символов, считанных при распознании токена. Если же указан тип omit, то определение токена не будет содержать в себе объекта iterator_range. Так что если следует игнорировать атрибут токена и притом действовать самым эффективным путём, то нужно явно указывать, что атрибут для данного токена отсутствует прописыванием типа omit в качестве типа атрибута, иначе хоть и будет работать так же замечательно, но просто чуть менее эффективно, так как в каждом токене придётся хранить эту пару итераторов.

Примеры использвания token_def и token:

namespace lex = boost::spirit::lex;

// this token expose the iterator range
// boost::iterator_range of the matched input sequence
lex::token_def<> something;
// this token does not have any attribute at all
lex::token_def omit_this;
// these tokens have an associated custom attribute type
lex::token_def number;
lex::token_def identifier;

// This is the token type being used by the lexer
// as you can see, every type listed in each of the token_def's
// above, is present in the mpl type sequence as the second
// template parameter to the lex::lexertl::token type
typedef lex::lexertl::token<
    std::string::const_iterator, /*or some other base iterator type*/
    mpl::vector
> TokenT;

Объявленный чуть выше тип TokenT должен быть предоставлен соответствующему классу лексера в качестве шаблонного параметра. Из самих классов лексеров обычно используются lex::lexertl::lexer и lex::lexertl::actor_lexer, разница между ними в том, что для первого нельзя прицеплять к объявлениям токенов семантические действия (так называемые actors), а для второго можно, как с использованием рукописных функций, функторов, так и с помощью Boost.Phoenix, включая ленивые функции и частичное применение с помощью phoenix::bind.

Для токенизации используется три основных функции: tokenize, работающая только с лексическим анализатором, и tokenize_and_parse вместе с tokenize_and_phrase_parse, которые предназначены для использования лексера совместно с парсерами Spirit.Qi. Там всё, в принципе, достаточно похоже на то, как это выглядит в случае Spirit.Qi и функцией parse или phrase_parse, так что особо на этом останавливаться не буду. Кстати, для Spirit.Lex очень неплохо составлена документация, есть много полезных и интересных примеров, которые легко приспособить к реальной жизни, всё разложено по полочкам, так что проблем с отысканием прототипов для этих функций и примеров их использования быть не должно.

Применение лексеров на практике обычно сводится к следующему:

  1. Пишем шаблон для класса, наследующийся от базового класса лексера (представлен классом boost::spirit::lex::lexer<T>).
  2. внутри класса делаем необходимые объявления token_def для используемых токенов (подобно правилам в грамматиках в Spirit.Qi).
  3. В конструкторе для каждого определения токена пишем распознаваемый паттерн с помощью регулярных выражений, записанных с помощью строк (не забываем экранировать обратные слеши — в C++ нет verbatim строк!). Справка по поводу допустимых регулярных выражений есть в документации.
  4. Также в конструкторе добавляем определения токенов к лексеру с помощью operator=, operator+= или члена-функции add для поля self, выглядит это обычно так: self.add(token1)...(tokenN);
  5. Далее определить тип токена для лексического анализатора и тип самого лексера, потом подсунуть получившийся базовый тип лексера в шаблонный параметр к написанному лексеру.
  6. Воспользоваться одной из функций tokenize, tokenize_and_parse или tokenize_and_phrase_parse для проведения лексического анализа (возможно, совместно с парсингом выражения).

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

///////////////////////////////////////////
// lexer.hpp

#pragma once

// INCLUDE lex::lexer, lex::token_def and lex::min_token_id
#include 
// INCLUDE lexertl::actor_lexer and lexertl::token
#include 
// INCLUDE boost::iterator_range
#include 

#include 

// ID_ANY is an identifier for the tokens
// that do not match any special regexp pattern.
// That is, these tokens are being constructed
// from arbitrary char sequences, corresponding
// to the "." regular expression.
enum tokenids
    {
    ID_ANY = boost::spirit::lex::min_token_id + 10
    };

// MyLexer TEMPLATE CLASS
template 
struct MyLexer
    : boost::spirit::lex::lexer
    {
    // Token definition for the tokens
    // that don't have any attribute value at all.
    typedef boost::spirit::lex::token_def<
        boost::spirit::lex::omit, char
    > ConsumedToken;
    // These tokens have an attribute value of type
    // iterator_range
    typedef boost::spirit::lex::token_def<
        boost::iterator_range, char
    > Token;

    MyLexer();
    // maybe, variable definition rule, eh?
    ConsumedToken VariableDef;
    // some ordinary puncuation symbols
    ConsumedToken OpenBracket;
    ConsumedToken CloseBracket;
    ConsumedToken OpenBrace;
    ConsumedToken CloseBrace;
    ConsumedToken OpenParens;
    ConsumedToken CloseParens;
    ConsumedToken Comma;
    ConsumedToken Assignment;
    // token definition representing
    // a comment pattern
    ConsumedToken Comment;
    // whitespace skipping, no need for a skipper parser, yeah!
    ConsumedToken Whitespace;
    // these are usual identifiers
    // that represent variable names
    Token StringIdentifier;
    // Some fundamental-typed constants
    Token StringLiteral;
    Token IntegerLiteral;
    Token RealLiteral;
    };

typedef boost::spirit::lex::lexertl::token<
    std::string::const_iterator
> TokenType;
typedef boost::spirit::lex::lexertl::actor_lexer<
    TokenType
> LexerType;

typedef MyLexer<
    LexerType
> MyLexerType;
///////////////////////////////////////////
// lexer.cpp

#include "lexer.hpp"

template 
MyLexer::MyLexer()
    {
    VariableDef = "\"variable\"";

    StringIdentifier = "[a-zA-Z]\\\\w*";
    StringLiteral = "\\\\\"[^\\\\\"]*\\\\\"";
    IntegerLiteral = "[\\\\-]?[0-9]+";
    RealLiteral = "[\\\\-]?[0-9]+\".\"[0-9]+";

    Comment = "\"//\".*$";
    self += Comment
            [boost::spirit::lex::_pass
                = boost::spirit::lex::pass_flags::pass_ignore];
    Whitespace = "\\\\s+";
    self += Whitespace
            [boost::spirit::lex::_pass
                = boost::spirit::lex::pass_flags::pass_ignore];

    OpenBracket = "\\\\{";
    CloseBracket = "\\\\}";
    OpenParens = "\\\\(";
    CloseParens = "\\\\)";
    Comma = "\",\"";
    Assignment = "\"=\"";
    OpenBrace = "\\\\[";
    CloseBrace = "\\\\]";

    self.add
        (Comma)
        (OpenParens)
        (CloseParens)
        (OpenBracket)
        (CloseBracket)
        (Assignment)
        (OpenBrace)
        (CloseBrace)
        (RealLiteral)
        (StringLiteral)
        (IntegerLiteral)
        (VariableDef)
        (StringIdentifier)
        (".", ID_ANY)
        ;
    }

Сначала мы определяем два типа токенов: ConsumedToken, которые просто съедаются при прохождении лексического анализатора, не предоставляя никакого атрибута при распознании, сюда относятся различные разделители: круглые, квадратные и фигурные скобки, знак присваивания, запятая, объявление переменной с помощью ключевого слова "variable"; и Token, которые оставляют после себя значение атрибута, сюда относятся строковые и целочисленные литералы, которые возвращают диапазон символов, задаваемый двумя итераторами, указывающими на начало и конец последовательности распознанных символов. Конечно, целочисленные литералы возможно было бы объявить и с типом атрибута вроде int или double но особой роли это не играет, необходимые преобразования типов можно сделать уже на этапе парсинга. Также есть ещё два паттерна, которые не имеют атрибута: токен, включающий в себя последовательность пробельных символов и ещё один, который включает в себя описание обычных однострочных комментариев. С ними особо ничего особенного не происходит за исключением одного момента: каждому из этих двух токенов назначено семантическое действие, которое имеет следующий вид:

[boost::spirit::lex::_pass
    = boost::spirit::lex::pass_flags::pass_ignore];

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

  • _start — итератор на начало цепочки распознанной последовательности символов.
  • _end — итератор, указывающий на заграничный (past the end) элемент в распознанной последовательности символов.
  • _pass — обозначает значение, указывающее на результат семантического действия. По-умолчанию равно значению флага lex::pass_flags::pass_normal. Если _pass установлен в значение lex::pass_flags::pass_fail, то лексер будет вести себя так, будто бы никакого токена не было распознано, точнее распознание токена завершилось неудачей. В случае, если флаг установлен в значение lex::pass_flags::pass_ignore, лексер проигнорирует текущее совпадение и продолжит пытаться дальше распознавать токены во входном потоке.
  • _tokenid — возвращает идентификатор для генерируемого токена.
  • _val — значение, из которого будет инициализирован следующий токен.
  • _state — состояние лексера, в котором была считана последовательность символов из входного потока.
  • _eoi — конечный итератор для входного потока лексера (от «end of input»).

В приведённом семантическом действии фигурирует заместитель _pass, которому присваивается флаг lex::pass_flags::pass_ignore, что означает, что лексер просто будет пропускать подобные токены и сразу же пытаться распознать что-то другое. Как раз то, что нужно для комментариев! Собственно для них это семантическое действие и используется. Определение токена Whitespace снабжено таким же действием для того, чтобы избавиться от необходимости использовать парсер пробелов в грамматиках тогда, когда дело дойдёт до парсинга.

После описания самих паттернов через регулярные выражения мы добавляем объявления токенов к лексеру через вызов self.add. Далее определяем тип токена для лексера через тип lex::lexertl::token, и тип лексера, в качестве которого выбираем lex::lexertl::actor_lexer (используется именно actor_lexer заместо простого lexer потому что у нас есть парочка токенов, для которых описаны семантические действия; обычный lexer не имеет возможности определять семантические действия в описаниях токенов). И, в конце концов, определяем наш лексер с шаблонным параметром actor_lexer<TokenType>.

Я уже упоминал тот факт, что Spirit.Lex имеет возможности для создания статических анализаторов наряду с динамическими. Естественно, использование статического анализатора — довольно хорошая оптимизация в том плане, что нужные таблицы для разбора будут сгенерированы во время компиляции кода и нам не придётся тратить на это время в процессе исполнения программы, имея полностью готовый к работе конечный автомат. Spirit.Lex сделан таким образом, что преобразование динамического лексера в статический может быть сделано предельно простым. Для того, чтобы сгенерировать код для статического анализатора, достаточно воспользоваться функцией lexertl::generate_static_dfa, а потом вместо lexertl::lexer пользоваться классом lexertl::static_lexer в который одним из шаблонных параметров нужно передать сгенерированный тип статического анализатора. За наглядным примером смотрим в документацию, там всё чётко расписано.

Чуть-чуть расширяем калькулятор.

Ну и напоследок, как я обещал, маленький пример того, как можно расширять функционал калькулятора арифметических выражений, добавляя, например, свои пользовательские функции, притом достаточно простым способом. Приведённый ниже код является логическим продолжением того, чем мы занимались во второй части, только с добавлением возможности использования определённого набора функций. Здесь я ограничился тем, что атрибуты выражений имеют тип double, но на деле можно пойти чуть дальше и зашаблонить тип для вычислений на некоторый произвольный, например, если стоит задача увеличения точности вычислений и того, что даёт тип double не хватает. Мне вот просто было лень это делать, так что уж как-нибудь сами, добавить дополнительный шаблонный параметр к грамматике — дело не сложное. 🙂
Я специально не стал добавлять все доступные функции из стандартного заголовочного файла <cmath>, ибо их там много, да и каждый желающий самостоятельно без труда может все прототипы отыскать.

Основная работа ложится на X-макросы, которые я в достаточной степени объяснял в третьей части. С их помощью определяются ленивые phoenix функции для использования в семантических действиях и сами правила с использованием этих ленивых функций. Чисто для красоты добавлена поддержка математических констант pi и e с помощью средств библиотеки boost::math, опять же без труда можно добавить и дополнительные константы.

Вот, собственно, и сам код:

#define BOOST_SPIRIT_USE_PHOENIX_V3
#include 
#include 

#include 

#include 

namespace detail { namespace __impl {

#define FOREACH_UNARY_FN(apply) \\
    apply(exp)  \\
    apply(sin)  \\
    apply(cos)  \\
    apply(tan)  \\
    apply(log)  \\
    apply(sqrt) \\
    apply(abs) 

#define FOREACH_BINARY_FN(apply) \\
    apply(pow)                   

#define DEFINE_FN(id)            \\
struct id ## _impl               \\
    {                            \\
    template         \\
    struct result                \\
        {                        \\
        typedef T type;          \\
        };                       \\
                                 \\
    template         \\
    T operator() (T value) const \\
        {                        \\
        return (std::id)(value); \\
        }                        \\
    };

// define unary function_impls
FOREACH_UNARY_FN(DEFINE_FN)
#undef DEFINE_FN

#define DEFINE_FN(id)                         \\
struct id ## _impl                            \\
    {                                         \\
                                              \\
    template <                                \\
          typename This                       \\
        , typename Arg1                       \\
        , typename Arg2                       \\
    >                                         \\
    struct result           \\
        {                                     \\
        typedef Arg1 type;                    \\
        };                                    \\
                                              \\
    template      \\
    _Tx operator()    ( _Tx arg1              \\
                      , _Ty arg2) const       \\
            {                                 \\
            return (std::id)(arg1, arg2);     \\
            }                                 \\
    };

// define binary function_impls

FOREACH_BINARY_FN(DEFINE_FN)
#undef DEFINE_FN

// make unary and binary impls lazy phoenix functions
#define DEFINE_PHOENIX_LAZY_FUNCTION(id) \\
    boost::phoenix::function const id = id ## _impl();

FOREACH_UNARY_FN(DEFINE_PHOENIX_LAZY_FUNCTION)
FOREACH_BINARY_FN(DEFINE_PHOENIX_LAZY_FUNCTION)

#undef DEFINE_PHOENIX_LAZY_FUNCTION

} // namespace detail::__impl

namespace spirit = boost::spirit;
namespace qi = spirit::qi;
namespace phx = boost::phoenix;
namespace consts = boost::math::constants;

#define UNARY_RULE(id) \\
    | qi::no_case[#id] >> '(' >> expr [ qi::_val = __impl::id(qi::_1) ] >> ')'
#define BINARY_RULE(id) \\
    | (qi::no_case[#id] >> '(' >> expr >> ',' >> expr >> ')') \\
      [ qi::_val = __impl::id(qi::_1, qi::_2) ]

template 
struct calc_grammar
    : qi::grammar
    {
    calc_grammar()
        : calc_grammar::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::double_     [ qi::_val =  qi::_1 ]
            |   '(' >> expr     [ qi::_val =  qi::_1 ] >> ')'
            |   '-' >> factor   [ qi::_val = -qi::_1 ]
            |   '+' >> factor   [ qi::_val =  qi::_1 ]
            |   qi::no_case["pi"] [ qi::_val = consts::pi() ]
            |   qi::no_case["e" ] [ qi::_val = consts::e()  ]
                FOREACH_UNARY_FN(UNARY_RULE)
                FOREACH_BINARY_FN(BINARY_RULE)
            ;
        }

    qi::rule
        expr, term, factor;
    };

#undef UNARY_RULE
#undef BINARY_RULE

#undef FOREACH_UNARY_FN
#undef FOREACH_BINARY_FN
} // namespace detail

8 Comments

  1. Ответить
    dips 11.09.2011

    Огромное спасибо! Очень полезный и интересный цикл статей по boost::spirit-2. Реально помогло понять phenix, да и вообще много нового узнал за один вечер. С нетерпением жду 6 части где Вы обещали написать о spirit::karma.

  2. Ответить
    Bratello74 17.10.2011

    Присоединяюсь.
    Отличные статьи. Я бы даже сказал, чуть ли не единственные в своём роде.
    Кирилл проделал огромный работу.
    Спасибо!
    (Про Карму — очень жду…)

  3. Ответить
    Kiri11 17.10.2011

    Скажите спасибо Man Manson’у. Без него эти статьи бы не появились на свет. А я, так сказать, просто помогаю с размещением)

  4. Ответить
    Владимир 21.11.2011

    Хорошие статьи ! Как мед, так и ложкой. Сбросьте пожалуйста вариант парсера для calc_grammar

  5. Ответить
    Anri 12.12.2011

    что-то я не понял, кто автор: Man Manson или Вы — кирилл?

    • Ответить
      Kiri11 03.01.2012

      Man Manson, это указано в подзаголовке статьи.

  6. Ответить
    aekirn@gmail.com 26.01.2015

    Здравствуйте Кирилл, очень интересная статья, хотел спросить а как быть допустим если хочу распарсить выражение 2x имея ввиду что это 2*x но без *

    вроде 2x это терм, 2 это factor и х factor но они не соединяются явным значком?

  7. Ответить
    aekirn@gmail.com 26.01.2015

    есть ли у Вас e-mail?

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

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