Да. Вот так. Я хочу иметь возможность вставить выражение, как:
"a && b || c"
непосредственно в исходный код в виде строки:
const std::string expression_text("a && b || c");
Создайте лениво оцененную структуру с этим:
Expr expr(magical_function(expression_text));
затем в дальнейшем оцениваем подстановку в известные значения:
evaluate(expr, a, b, c);
Я хотел бы расширить этот маленький DSL позже, поэтому сделаю что-то более сложное, используя некоторый синтаксис не-C ++, чтобы я не мог просто жестко закодировать свое выражение простым способом. Сценарий использования состоит в том, что я смогу копировать и вставлять ту же логику из другого модуля, используемого в другой области разработки для другого языка, вместо того, чтобы каждый раз адаптировать ее к синтаксису C ++.
Если бы кто-то мог начать меня хотя бы с того, как сделать вышеупомянутую простую концепцию 1 выражения и 2 логических операторов, это было бы очень полезно.
Примечание: я разместил этот вопрос из-за обратной связи с другим вопросом, который я разместил: Как проанализировать ввод DSL в высокопроизводительный шаблон выражения. Здесь я на самом деле хотел получить ответ на немного иную проблему, но комментарии спровоцировали этот конкретный вопрос, который, как мне показалось, стоит опубликовать, поскольку потенциальные ответы действительно заслуживают документирования.
Отказ от ответственности: я ничего не знаю о metaparse, и очень мало о прото. Следующий код — моя попытка (в основном методом проб и ошибок) изменить этот пример сделать что-то похожее на то, что вы хотите.
Код может быть легко разделен на несколько частей:
typedef token < lit_c < 'a' > > arg1_token;
typedef token < lit_c < 'b' > > arg2_token;
typedef token < lit_c < 'c' > > arg3_token;
token<Parser>
: Parser
вParser
, lit_c<char>
: char
и результатalways
,typedef token < keyword < _S ( "true" ), bool_<true> > > true_token;
typedef token < keyword < _S ( "false" ), bool_<false> > > false_token;
keyword<metaparse_string,result_type=undefined>
: metaparse_string
(_S("true")
возвращаетсяmetaparse::string<'t','r','u','e'>
это то, что метапарсис использует внутриresult_type
,typedef token < keyword < _S ( "&&" ) > > and_token;
typedef token < keyword < _S ( "||" ) > > or_token;
typedef token < lit_c < '!' > > not_token;
В случае and_token
а также or_token
результат не определен и в грамматике ниже он игнорируется.
struct paren_exp;
Первый paren_exp
объявлен заранее.
typedef one_of<
paren_exp,
transform<true_token, build_value>,
transform<false_token, build_value>,
always<arg1_token, arg<0> >,
always<arg2_token, arg<1> >,
always<arg3_token, arg<2> >
>
value_exp;
one_of<Parsers...>
: transform<Parser,SemanticAction>
: Parser
, Тип результата является типом результата Parser
преобразовано SemanticAction
,always<Parser,NewResultType>
:
Матчи Parser
, возвращает NewResultType
,
Эквивалентное духовное правило будет:
value_exp = paren_exp [ _val=_1 ]
| true_token [ _val=build_value(_1) ]
| false_token [ _val=build_value(_1) ]
| argN_token [ _val=phx::construct<arg<N>>() ];
typedef one_of<
transform<last_of<not_token, value_exp>, build_not>,
value_exp
>
not_exp;
last_of<Parsers...>
:
last_of соответствует каждому из Parsers
в последовательности и его тип результата является типом результата последнего парсера.
Эквивалентное духовное правило будет:
not_exp = (omit[not_token] >> value_exp) [ _val=build_not(_1) ]
| value_exp [ _val=_1 ];
typedef
foldl_start_with_parser<
last_of<and_token, not_exp>,
not_exp,
build_and
> and_exp; // and_exp = not_exp >> *(omit[and_token] >> not_exp);
typedef
foldl_start_with_parser<
last_of<or_token, and_exp>,
and_exp,
build_or
> or_exp; // or_exp = and_exp >> *(omit[or_token] >> and_exp);
foldl_start_with_parser<RepeatingParser,InitialParser,SemanticAction>
:
этот парсер комбинатор совпадений InitialParser
а потом RepeatingParser
несколько раз, пока не получится. Тип результата является результатом mpl::fold<RepeatingParserSequence, InitialParserResult, SemanticAction>
, где RepeatingParserSequence
представляет собой последовательность типов результатов каждого приложения RepeatingParser
, Если RepeatingParser
никогда не удается, тип результата просто InitialParserResult
,
Я верю (xd), что эквивалентное духовное правило будет:
or_exp = and_exp[_a=_1]
>> *( omit[or_token] >> and_exp [ _val = build_or(_1,_a), _a = _val ]);
struct paren_exp: middle_of < lit_c < '(' > , or_exp, lit_c < ')' > > {};
// paren_exp = '(' >> or_exp >> ')';
middle_of<Parsers...>
: Parsers
и тип результата является результатом синтаксического анализатора, который находится в середине.typedef last_of<repeated<space>, or_exp> expression;
//expression = omit[*space] >> or_exp;
repeated<Parser>
: Parser
многократно. Результатом является последовательность типов результатов каждого приложения синтаксического анализатора, если анализатор завершается неудачно с первой попытки, результатом является пустая последовательность.typedef build_parser<entire_input<expression> > function_parser;
Эта строка создает метафункцию, которая принимает входную строку и возвращает результат анализа.
Давайте посмотрим на пример прохождения построения выражения. Это делается в два этапа: сначала грамматика создает дерево, которое зависит от build_or
, build_and
, build_value
, build_not
а также arg<N>
, Как только вы получите этот тип, вы можете получить прото-выражение, используя proto_type
ЬурейеЕ.
«a ||! b»
Мы начинаем на or_expr
:
or_expr
: Мы пробуем его InitialParser, который and_expr
, and_expr
: Мы пробуем его InitialParser, который not_expr
, not_expr
: not_token терпит неудачу, поэтому мы пытаемся value_expr
, value_expr
: arg1_token успешен. Тип возврата arg<0>
и мы вернемся к not_expr
,not_expr
: тип возврата не изменяется на этом этапе. Мы возвращаемся к and_expr
, and_expr
: Мы пробуем его RepeatingParser, он терпит неудачу. and_expr успешно выполняется, и его тип возвращаемого значения является типом возвращаемого значения InitialParser: arg<0>
. Мы возвращаемся к or_expr
, or_expr
: Мы пробуем его совпадения RepeatingParser, or_token, мы пытаемся and_expr
, and_expr
: Попробуем его InitialParser not_expr
,not_expr
: not_token удаётся, мы пытаемся value_expr
, value_expr
: arg2_token успешен. Тип возврата arg<1>
и мы вернемся к not_expr
, not_expr
: тип возвращаемого значения изменяется преобразованием с использованием build_not: build_not :: применять< Arg<1>>. Мы возвращаемся к and_expr
, and_expr
: Мы пробуем его RepeatingParser, он терпит неудачу. and_expr успешно завершается и возвращается build_not :: применять< Arg<1>>. Мы возвращаемся к or_expr
, or_expr
: RepeatingParser завершился успешно, foldlp использует build_or на build_not::apply< arg<1> >
а также arg<0>
, получение build_or::apply< build_not::apply< arg<1> >, arg<0> >
. Как только мы построим это дерево, мы получим его proto_type
:
build_or::apply< build_not::apply< arg<1> >, arg<0> >::proto_type;
proto::logical_or< arg<0>::proto_type, build_not::apply< arg<1> >::proto_type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, build_not::apply< arg<1> >::proto_type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, proto::logical_not< arg<1>::proto_type >::type >::type;
proto::logical_or< proto::terminal< placeholder<0> >::type, proto::logical_not< proto::terminal< placeholder<1> >::type >::type >::type;
#include <iostream>
#include <vector>
#include <boost/metaparse/repeated.hpp>
#include <boost/metaparse/sequence.hpp>
#include <boost/metaparse/lit_c.hpp>
#include <boost/metaparse/last_of.hpp>
#include <boost/metaparse/middle_of.hpp>
#include <boost/metaparse/space.hpp>
#include <boost/metaparse/foldl_start_with_parser.hpp>
#include <boost/metaparse/one_of.hpp>
#include <boost/metaparse/token.hpp>
#include <boost/metaparse/entire_input.hpp>
#include <boost/metaparse/string.hpp>
#include <boost/metaparse/transform.hpp>
#include <boost/metaparse/always.hpp>
#include <boost/metaparse/build_parser.hpp>
#include <boost/metaparse/keyword.hpp>
#include <boost/mpl/apply_wrap.hpp>
#include <boost/mpl/front.hpp>
#include <boost/mpl/back.hpp>
#include <boost/mpl/bool.hpp>
#include <boost/proto/proto.hpp>
#include <boost/fusion/include/at.hpp>
#include <boost/fusion/include/make_vector.hpp>
using boost::metaparse::sequence;
using boost::metaparse::lit_c;
using boost::metaparse::last_of;
using boost::metaparse::middle_of;
using boost::metaparse::space;
using boost::metaparse::repeated;
using boost::metaparse::build_parser;
using boost::metaparse::foldl_start_with_parser;
using boost::metaparse::one_of;
using boost::metaparse::token;
using boost::metaparse::entire_input;
using boost::metaparse::transform;
using boost::metaparse::always;
using boost::metaparse::keyword;
using boost::mpl::apply_wrap1;
using boost::mpl::front;
using boost::mpl::back;
using boost::mpl::bool_;struct build_or
{
typedef build_or type;
template <class C, class State>
struct apply
{
typedef apply type;
typedef typename boost::proto::logical_or<typename State::proto_type, typename C::proto_type >::type proto_type;
};
};
struct build_and
{
typedef build_and type;
template <class C, class State>
struct apply
{
typedef apply type;
typedef typename boost::proto::logical_and<typename State::proto_type, typename C::proto_type >::type proto_type;
};
};template<bool I>
struct value //helper struct that will be used during the evaluation in the proto context
{};
struct build_value
{
typedef build_value type;
template <class V>
struct apply
{
typedef apply type;
typedef typename boost::proto::terminal<value<V::type::value> >::type proto_type;
};
};
struct build_not
{
typedef build_not type;
template <class V>
struct apply
{
typedef apply type;
typedef typename boost::proto::logical_not<typename V::proto_type >::type proto_type;
};
};
template<int I>
struct placeholder //helper struct that will be used during the evaluation in the proto context
{};
template<int I>
struct arg
{
typedef arg type;
typedef typename boost::proto::terminal<placeholder<I> >::type proto_type;
};
#ifdef _S
#error _S already defined
#endif
#define _S BOOST_METAPARSE_STRING
typedef token < keyword < _S ( "&&" ) > > and_token;
typedef token < keyword < _S ( "||" ) > > or_token;
typedef token < lit_c < '!' > > not_token;
typedef token < keyword < _S ( "true" ), bool_<true> > > true_token;
typedef token < keyword < _S ( "false" ), bool_<false> > > false_token;
typedef token < lit_c < 'a' > > arg1_token;
typedef token < lit_c < 'b' > > arg2_token;
typedef token < lit_c < 'c' > > arg3_token;struct paren_exp;
typedef
one_of< paren_exp, transform<true_token, build_value>, transform<false_token, build_value>, always<arg1_token, arg<0> >, always<arg2_token, arg<1> >, always<arg3_token, arg<2> > >
value_exp; //value_exp = paren_exp | true_token | false_token | arg1_token | arg2_token | arg3_token;
typedef
one_of< transform<last_of<not_token, value_exp>, build_not>, value_exp>
not_exp; //not_exp = (omit[not_token] >> value_exp) | value_exp;
typedef
foldl_start_with_parser <
last_of<and_token, not_exp>,
not_exp,
build_and
>
and_exp; // and_exp = not_exp >> *(and_token >> not_exp);
typedef
foldl_start_with_parser <
last_of<or_token, and_exp>,
and_exp,
build_or
>
or_exp; // or_exp = and_exp >> *(or_token >> and_exp);
struct paren_exp: middle_of < lit_c < '(' > , or_exp, lit_c < ')' > > {}; //paren_exp = lit('(') >> or_exp >> lit('(');
typedef last_of<repeated<space>, or_exp> expression; //expression = omit[*space] >> or_exp;
typedef build_parser<entire_input<expression> > function_parser;template <typename Args>
struct calculator_context
: boost::proto::callable_context< calculator_context<Args> const >
{
calculator_context ( const Args& args ) : args_ ( args ) {}
// Values to replace the placeholders
const Args& args_;
// Define the result type of the calculator.
// (This makes the calculator_context "callable".)
typedef bool result_type;
// Handle the placeholders:
template<int I>
bool operator() ( boost::proto::tag::terminal, placeholder<I> ) const
{
return boost::fusion::at_c<I> ( args_ );
}
template<bool I>
bool operator() ( boost::proto::tag::terminal, value<I> ) const
{
return I;
}
};
template <typename Args>
calculator_context<Args> make_context ( const Args& args )
{
return calculator_context<Args> ( args );
}
template <typename Expr, typename ... Args>
int evaluate ( const Expr& expr, const Args& ... args )
{
return boost::proto::eval ( expr, make_context ( boost::fusion::make_vector ( args... ) ) );
}
#ifdef LAMBDA
#error LAMBDA already defined
#endif
#define LAMBDA(exp) apply_wrap1<function_parser, _S(exp)>::type::proto_type{}
int main()
{
using std::cout;
using std::endl;
cout << evaluate ( LAMBDA ( "true&&false" ) ) << endl;
cout << evaluate ( LAMBDA ( "true&&a" ), false ) << endl;
cout << evaluate ( LAMBDA ( "true&&a" ), true ) << endl;
cout << evaluate ( LAMBDA ( "a&&b" ), true, false ) << endl;
cout << evaluate ( LAMBDA ( "a&&(b||c)" ), true, false, true ) << endl;
cout << evaluate ( LAMBDA ( "!a&&(false||(b&&!c||false))" ), false, true, false ) << endl;
}
/*int main(int argc , char** argv)
{
using std::cout;
using std::endl;
bool a=false, b=false, c=false;
if(argc==4)
{
a=(argv[1][0]=='1');
b=(argv[2][0]=='1');
c=(argv[3][0]=='1');
}
LAMBDA("a && b || c") expr;
cout << evaluate(expr, true, true, false) << endl;
cout << evaluate(expr, a, b, c) << endl;
return 0;
}*/
В течение долгого времени синтаксический анализ во время компиляции означал использование метапрограммирования шаблонов — что кажется линейным шумом большинству начинающих и даже промежуточных программистов на C ++.
Однако с C ++ 11 мы получили constexpr, а в C ++ 14 было снято много ограничений на constexpr. C ++ 17 даже делает некоторые из стандартных вещей библиотеки constexpr.
Пытаясь изучить продвинутый современный C ++, я решил написать HTML-парсер времени компиляции. Идея заключалась в том, чтобы создать быстрый движок HTML-шаблонов.
Весь код можно найти здесь: https://github.com/rep-movsd/see-phit
Я кратко объясню то, что я узнал, когда заставил это работать.
Обработка динамических структур данных
Мне нужно было проанализировать const char * и преобразовать его в многопоточное дерево — но динамическое распределение не имеет смысла в земле constexpr.
Решение?
Используйте массив узлов с индексами, указывающими на детей и братьев и сестер — по сути, как вы могли бы сделать это в FORTRAN!
Предостережение заключается в том, что ваш список узлов должен изначально иметь фиксированный размер. По-видимому, сохранение его очень большим, заставляет gcc значительно замедлять компиляцию.
Если вы в конечном итоге пройдете конец массива, компилятор выдаст ошибку.
Я написал небольшой std :: array как обертку, которая полностью constexpr.
анализ
Практически стандартный код, который вы пишете для разбора во время выполнения, будет работать во время компиляции!
Петли, рекурсия, условные выражения — все работает отлично.
Одна проблема была — как представлять строки? Использование вышеупомянутого подхода — массива char — очень жадно потребляет память и утомительно.
К счастью, в моем случае все, что мне когда-либо было нужно, это просто подстроки исходного ввода const char *.
Итак, я написал небольшой класс типа constexpr string_view, который просто содержит указатели на начало и конец соответствующего проанализированного токена.
Создание новых литеральных строк — это всего лишь вопрос превращения этих представлений в константный литерал *.
Отчет об ошибках
Основной способ обработки ошибок в коде constexpr заключается в вызове функции, которая не является constexpr — компилятор останавливает и печатает строку, вызывающую сбой, которая может легко содержать строку ошибки.
Однако я хотел большего — я хотел, чтобы парсер также отображал строку и столбец. Я боролся некоторое время и в конце концов подумал, что это невозможно.
Но я вернулся к этому и попробовал все, что мог придумать. Наконец я нашел способ, который заставляет gcc печатать 2 числа и сообщение с описанием ошибки.
По сути, это включает создание шаблона с двумя целочисленными параметрами (row и col), значения которых поступают из анализатора constexpr.
Спектакль
Я не мог четко определить, какой тип кода constexpr замедляет работу компилятора, но производительность по умолчанию вовсе не слишком плохая. Я могу проанализировать HTML-файл 1000 узлов примерно за 1,5 секунды на GCC.
лязг немного быстрее.
Я намерен написать более подробное описание того, как работает код в вики репозитория github — следите за обновлениями.
Хотя технически возможно осуществлять такого рода метапрограммирование с помощью шаблонов или constexpr, я бы не рекомендовал такой подход. Вы получите огромное количество очень сложного кода. Быть сложным в отладке и дорогим в обслуживании и расширении.
Вместо этого используйте любой другой язык для генерации кода C ++ из ваших выражений.
Если вы используете Visual Studio, один хороший встроенный способ — это текстовые шаблоны T4. Вот больше деталей.
В противном случае используйте любой другой язык, доступный на вашей платформе, например, Я сделал нечто подобное в Python.