Как я могу использовать полиморфные атрибуты с парсерами boost::spirit::qi?
Я хотел бы, чтобы мой анализатор boost:: spirit мог анализировать файл, преобразовывать проанализированные правила в различные типы и выдавать вектор, содержащий все найденные совпадения. Все типы, которые передаются как атрибуты, должны быть унаследованы от базового типа, например:
#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/adapt_struct.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/foreach.hpp>
struct CommandBase
{
virtual void commandAction()
{
std::cout << "This is a base command. You should never see this!" << std::endl;
//Boost::spirit seems to get mad if I make this purely virtual. Clearly I'm doing it wrong.
}
};
struct CommandTypeA : public CommandBase
{
int valueA;
int valueB;
virtual void commandAction()
{
std::cout << "CommandType A! ValueA: " << valueA << " ValueB: " << valueB << std::endl;
}
};
struct CommandTypeB : public CommandBase
{
double valueA;
std::vector<char> valueB;
virtual void commandAction()
{
std::cout << "CommandType B! valueA: " << valueA << " string: " << std::string(valueB.begin(), valueB.end()) << std::endl;
}
};
struct CommandTypeC : public CommandBase
{
//Represents a sort of "subroutine" type where multiple commands can be grouped together
std::vector<char> labelName;
std::vector<boost::shared_ptr<CommandBase> > commands;
virtual void commandAction()
{
std::cout << "Subroutine: " << std::string(labelName.start(), labelName.end())
<< " has " << commands.size() << " commands:" << std::endl;
BOOST_FOREACH(boost::shared_ptr<CommandBase> c, commands)
{
c->commandAction();
}
}
};
Теперь мой код парсера:
namespace ascii = boost::spirit::ascii;
namespace qi = boost::spirit::qi;
using qi::lit_;
BOOST_FUSION_ADAPT_STRUCT(
CommandTypeA,
(int, valueA)
(int, valueB)
)
BOOST_FUSION_ADAPT_STRUCT(
CommandTypeB,
(double, valueA)
(std::vector<char>, valueB)
)
BOOST_FUSION_ADAPT_STRUCT(
CommandTypeC,
(std::vector<char>, labelName)
(std::vector<boost::shared_ptr<CommandBase> >, commands)
)
template<typename Iterator, typename Skipper = ascii::space_type>
struct CommandParser : qi::grammar<Iterator, std::vector<boost::shared_ptr<CommandBase> >(), Skipper>
{
public:
CommandParser() : CommandParser()::base_type(commands)
{
CommandARule = qi::int_ >> qi::int_ >> lit("CMD_A");
CommandBRule = qi::int_ >> +(qi::char_) >> lit("CMD_B");
CommandCRule = qi::char_(':') >> lexeme[+(qi::char_ - ';' - ascii::space) >> +ascii::space] >> commands >> qi::char_(';');
commands = +(CommandARule | CommandBRule | CommandCRule);
}
protected:
qi::rule<Iterator, boost::shared_ptr<CommandTypeA>, Skipper> CommandARule;
qi::rule<Iterator, boost::shared_ptr<CommandTypeB>, Skipper> CommandBRule;
qi::rule<Iterator, boost::shared_ptr<CommandTypeC>, Skipper> CommandCRule;
qi::rule<Iterator, std::vector<boost::shared_ptr<CommandBase> >, Skipper> commands;
};
std::vector<boost::shared_ptr<CommandBase> > commandList;
bool success = qi::phrase_parse(StartIterator, EndIterator, CommandParser, ascii::space, commandList);
BOOST_FOREACH(boost::shared_ptr<CommandBase> c, commandList)
{
c->commandAction();
}
Теперь этот код определенно не будет компилироваться, но я надеюсь, что он поможет понять, что я пытаюсь сделать.
Основное зависание заключается в том, что qi::rules, похоже, хочет выдавать реальную структуру, а не ссылку на нее.
Мой вопрос таков:
Можно ли заставить qi:: rule выдавать совместимую с полиморфизмом ссылку, как я пытаюсь (если да, как), и является ли это лучшим подходом к тому, что я пытаюсь выполнить (а именно список исполняемых объектов, представляющих разобранные команды и их параметры)?
1 ответ
Spirit намного дружелюбнее к полиморфизму времени компиляции
typedef variant<Command1, Command2, Command3> Command;
Но давайте предположим, что вы действительно хотите заняться старомодным полиморфизмом...
Однако просто обновление полиморфных объектов на лету во время анализа - это верный способ
- сделай свой парсер раздутым с помощью семантических действий
- создать много утечек памяти при обратном отслеживании в правилах грамматики
- сделать синтаксический анализ очень медленным (потому что у вас есть все способы динамического размещения).
- Хуже всего то, что ничего из этого не будет оптимизировано, даже если вы на самом деле не передаете ссылку на атрибут на верхний уровень.
parse
API. (Обычно вся обработка атрибутов "магически" испаряется во время компиляции, что очень полезно для проверки входного формата)
Таким образом, вы захотите создать держатель для объектов вашего базового класса команд или производных. Сделайте так, чтобы держатель удовлетворял RuleOfZero и вывел фактическое значение по типу стирания.
(Помимо решения "случайной" сложности и ограничения по восстановлению памяти, бонус к этой абстракции заключается в том, что вы все еще можете выбирать статическую обработку хранилища, поэтому вы экономите [много] времени при выделении кучи.)
Я посмотрю на ваш образец, чтобы увидеть, смогу ли я продемонстрировать это быстро.
Вот что я имею в виду с классом 'holder' (добавьте виртуальный деструктор в CommandBase
!):
struct CommandHolder
{
template <typename Command> CommandHolder(Command cmd)
: storage(new concrete_store<Command>{ std::move(cmd) }) { }
operator CommandBase&() { return storage->get(); }
private:
struct base_store {
virtual ~base_store() {};
virtual CommandBase& get() = 0;
};
template <typename T> struct concrete_store : base_store {
concrete_store(T v) : wrapped(std::move(v)) { }
virtual CommandBase& get() { return wrapped; }
private:
T wrapped;
};
boost::shared_ptr<base_store> storage;
};
Как вы можете видеть, я выбрал unique_ptr
для простоты владения семантикой здесь ( variant
позволит избежать некоторых накладных расходов в качестве оптимизации позже). Я не мог сделать unique_ptr
работать с Духом, потому что Дух просто не осознает движения. (Дух X3 будет).
Мы можем тривиально реализовать стертый тип AnyCommand
на основании этого держателя:
struct AnyCommand : CommandBase
{
template <typename Command> AnyCommand(Command cmd)
: holder(std::move(cmd)) { }
virtual void commandAction() override {
static_cast<CommandBase&>(holder).commandAction();
}
private:
CommandHolder holder;
};
Так что теперь вы можете "назначить" любую команду AnyCommand и использовать ее "полиморфно" через держатель, даже если держатель и AnyCommand имеют идеальную семантику значений.
Этот пример грамматики будет делать:
CommandParser() : CommandParser::base_type(commands)
{
using namespace qi;
CommandARule = int_ >> int_ >> "CMD_A";
CommandBRule = double_ >> lexeme[+(char_ - space)] >> "CMD_B";
CommandCRule = ':' >> lexeme [+graph - ';'] >> commands >> ';';
command = CommandARule | CommandBRule | CommandCRule;
commands = +command;
}
С правилами, определенными как:
qi::rule<Iterator, CommandTypeA(), Skipper> CommandARule;
qi::rule<Iterator, CommandTypeB(), Skipper> CommandBRule;
qi::rule<Iterator, CommandTypeC(), Skipper> CommandCRule;
qi::rule<Iterator, AnyCommand(), Skipper> command;
qi::rule<Iterator, std::vector<AnyCommand>(), Skipper> commands;
Это довольно восхитительное сочетание семантики значения и полиморфизма времени исполнения:)
Основное испытание
int main()
{
std::string const input =
":group \n"
" 3.14 π CMD_B \n"
" -42 42 CMD_A \n"
" -inf -∞ CMD_B \n"
" +inf +∞ CMD_B \n"
"; \n"
"99 0 CMD_A";
auto f(begin(input)), l(end(input));
std::vector<AnyCommand> commandList;
CommandParser<std::string::const_iterator> p;
bool success = qi::phrase_parse(f, l, p, qi::space, commandList);
if (success) {
BOOST_FOREACH(AnyCommand& c, commandList) {
c.commandAction();
}
} else {
std::cout << "Parsing failed\n";
}
if (f!=l) {
std::cout << "Remaining unparsed input '" << std::string(f,l) << "'\n";
}
}
Печать:
Subroutine: group has 4 commands:
CommandType B! valueA: 3.14 string: π
CommandType A! ValueA: -42 ValueB: 42
CommandType B! valueA: -inf string: -∞
CommandType B! valueA: inf string: +∞
CommandType A! ValueA: 99 ValueB: 0
Посмотреть все Live On Coliru