How can I use polymorphic attributes with boost::spirit::qi parsers?

Spirit is a lot friendlier to compiletime-polymorphism

typedef variant<Command1, Command2, Command3> Command;

But, let’s suppose you really want to do the old-fashioned polymorphism thing…

Just newing-up the polymorphic objects on the fly during parsing, however, is a sure-fire way to

  • make your parser bloated with semantic actions
  • create lot of memory leaks on back-tracking in the grammar rules
  • make parsing awesomely slow (because you have all manner of dynamic allocation going on).
  • Worst of all, none of this would be optimized away, even when you’re not actually passing an attribute reference into the top-level parse API. (Usually, all attribute handling “magically” vaporizes at compile-time, which is very useful for input format validation)

So you’ll want to create a holder for objects of your base-command class, or derived. Make the holder satisfy RuleOfZero and get the actual value out by type erasure.

(Beyond solving the “accidental” complexity and limits w.r.t. memory reclamation, a bonus to this abstraction is that you you can still opt to handle the storage statically, so you save [a lot] of time in heap allocations.)

I’ll look at your sample to see whether I can demonstrate it quickly.

Here is what I mean with a ‘holder’ class (add a virtual destructor to 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;
};

As you can see I opted for unique_ptr for simples ownership semantics here (a variant would avoid some allocation overhead as an optimization later). I couldn’t make unique_ptr work with Spirit because Spirit is simply not move-aware. (Spirit X3 will be).

We can trivially implement a type-erased AnyCommand based on this holder:

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;
};

So now you can “assign” any command to an AnyCommand and use it “polymorphically” through the holder, even though the holder and AnyCommand have perfect value-semantics.

This sample grammar will do:

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;
}

With the rules defined as:

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;

This is quite a delightful mix of value-semantics and runtime-polymorphism 🙂

The test main of

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";
    }
}

Prints:

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

See it all Live On Coliru

Leave a Comment