Boost.Spirit.x3 avoid collapsing two consecutive attributes of the same type into a vector
Asked Answered
D

1

9

I am trying to learn Boost.Spirit, but I have found a difficulty.

I am trying to parse a string into the following structure:

struct employee {
    std::string name;
    std::string location;
};

And it seems that when two attributes with the same type are back to back, they collapse (logically) into a std::vector of that type. Because of that rule, the following parser

+x3::ascii::alnum >>
    +x3::space >>
    +x3::ascii::alnum

would have the attribute of std::vector<std::string>.

But I am trying to parse this into that struct, which means that the ideal attribute for me would be a boost::fusion::tuple<std::string, std::string>, so I can adapt my struct to it.

The complete version of the not working code (referenced above):

// Example program
#include <iostream>
#include <string>

#include <boost/spirit/home/x3.hpp>
#include <boost/fusion/include/adapt_struct.hpp>


struct employee {
    std::string name;
    std::string location;
};

BOOST_FUSION_ADAPT_STRUCT(employee, 
    (std::string, name),
    (std::string, location)
)

namespace x3 = boost::spirit::x3;

x3::rule<struct parse_emp_id, employee> const parse_emp = "Employee Parser";
auto parse_emp_def = 
    +x3::ascii::alnum >>
    +x3::space >>
    +x3::ascii::alnum
    ;
BOOST_SPIRIT_DEFINE(parse_emp);

int main()
{
    std::string input = "Joe Fairbanks";
    
    employee ret;
    
    x3::parse(input.begin(), input.end(), parse_emp, ret);
    
    std::cout << "Name: " << ret.name << "\tLocation: " << ret.location << std::endl;
}

See it live

This code triggers a static_assert telling me that my attribute isn't correct:

error: static_assert failed "Attribute does not have the expected size."

With the command of

clang++ -std=c++14 test.cpp

(it also fails under GCC).

What I have tried

I have found a workaround to this problem, but it is messy, and I can't believe that this is the cleanest way:

// Example program
#include <iostream>
#include <string>

#include <boost/spirit/home/x3.hpp>
#include <boost/fusion/include/adapt_struct.hpp>


struct employee {
    std::string name;
    std::string location;
};

namespace x3 = boost::spirit::x3;

x3::rule<struct parse_emp_id, employee> const parse_emp = "Employee Parser";
auto parse_emp_def = 
    x3::eps [
    ([](auto& ctx) {
        x3::_val(ctx) = employee{};
    })
    ]>>
    (+x3::ascii::alnum)[
    ([](auto& ctx) {
        x3::_val(ctx).name = x3::_attr(ctx);
    })
    ]>>
    +x3::space >>
    (+x3::ascii::alnum)[
    ([](auto& ctx) {
        x3::_val(ctx).location = x3::_attr(ctx);
    })
    ]
    ;
BOOST_SPIRIT_DEFINE(parse_emp);

int main()
{
    std::string input = "Joe Fairbanks";
    
    employee ret;
    
    x3::parse(input.begin(), input.end(), parse_emp, ret);
    
    std::cout << "Name: " << ret.name << "\tLocation: " << ret.location << std::endl;
}

See it live

I really don't like that solution: it kinda ruins the amazing expressiveness of spirit and makes it super ugly, also if I want to add new fields into the employee struct, then I have to add an extra lambda, instead of just updating my BOOST_FUSION_ADAPT_STRUCT, which is much easier.

So the question is: Is there some way to (hopefully) cleanly split two consecutive attributes of the same type from the std::vector and into a boost::fusion::vector?

Thank you in advance for getting this far ;).

Doublestop answered 10/5, 2016 at 3:15 Comment(0)
P
6

The problem is that unlike character literals, x3::space has an attribute. So you don't have an attribute of two separate character sequences separated by whitespace, but rather an attribute of one big character sequence which includes the whitespace.

The omit directive is what you're after, and with that single addition your 'not working code' works. :-]

// Example program
#include <string>
#include <iostream>

#include <boost/fusion/include/adapt_struct.hpp>
#include <boost/spirit/home/x3.hpp>

namespace x3 = boost::spirit::x3;

struct employee {
    std::string name;
    std::string location;
};
BOOST_FUSION_ADAPT_STRUCT(employee, name, location)

x3::rule<struct parse_emp_id, employee> const parse_emp = "Employee Parser";
auto parse_emp_def
    =      +x3::ascii::alnum
        >>  x3::omit[+x3::space]
        >> +x3::ascii::alnum
    ;
BOOST_SPIRIT_DEFINE(parse_emp)

int main()
{
    std::string const input = "Joe Fairbanks";

    employee ret;
    x3::parse(input.begin(), input.end(), parse_emp, ret);

    std::cout << "Name: " << ret.name << "\tLocation: " << ret.location << '\n';
}

Online Demo

Polypoid answered 10/5, 2016 at 6:37 Comment(5)
It's a good day when the answer to a spirit question is already there before I find the question :) +1Onestep
@Onestep : Hah! This is the first I've beat you to an X3 question surely, but then, this was a very easy one. ;-]Polypoid
@Russel I'd suggest using a skipper instead of explicit whitespace (see #17073487). Live on Coliru.Onestep
I would expect the omit directive to just remove the whitespace from the character sequence, not splitting it into two. Why does this work?Doublestop
@RussellGreene : omit specifically has no attribute, which is the key. +x3::ascii::alnum >> +' ' >> +x3::ascii::alnum would work as well with no need for omit, because character literals also do not have an attribute.Polypoid

© 2022 - 2024 — McMap. All rights reserved.