Spirit Qi attribute propagation issue with single-member struct
Solution 1:
This is a quite infamous edge-case in Spirit. The problem is, special-case handling of single-element Fusion Sequences in Spirit breaks some abstractions.
The usual workaround is to adapt the exposed-attribute side to be less-trivial:
rule<It, single_member_struct()> r = eps >> XXX;
// the `eps` is there to break the spell
However, here that won't work, because your (a > XXX > b)
subexpression results in another vector1<decltype(member_type)>
and this time, no amount of smart parenthesizing or eps
-ing will save you.[1]
To cut the long story short, I have three workarounds:
1. #define KEEP_STRING_WORKAROUND
See it Live On Coliru
In which you'll simply allow gr_identifier
to return a std::wstring
[2]:
rule<It, std::string()> gr_identifier =
(alpha | '_') >> *(alnum | '_');
This effectively just postpones the magic attribute transformation that uses the Fusion adaptation if identifier
, and thereby breaks the spell:
rule<It, problem(), qi::space_type> gr_problem =
gr_identifier
>> gr_identifier
>> ('(' > gr_identifier > ')')
;
Just works. I think this is probably the least intrusive workaround
2. #define DUMMY_WORKAROUND
See it Live On Coliru
Whereby you unjinx the magix by ... making the identifier
struct not fusion adapted to a single-element fusion sequence. Yes. This involves the EvilHack™ of adding a dummy field. To minimize confusion, lets' make it qi::unused_type
though:
struct identifier
{
std::string name;
qi::unused_type dummy;
};
BOOST_FUSION_ADAPT_STRUCT(
identifier,
(std::string, name)
(qi::unused_type, dummy)
)
And now:
rule<It, identifier()> gr_identifier =
(alpha | '_') >> *(alnum | '_') >> attr(42); // that's hacky
Works
3. #define NO_ADAPT_WORKAROUND
See it Live On Coliru
The final workaround might be the most obvious: don't adapt the struct as a fusion sequence in the first place, and profit:
struct identifier
{
std::string name;
identifier() = default;
explicit identifier(std::string name)
: name(std::move(name))
{}
};
Note that to allow attribute propagation, now you will need suitable conversion constructors to be present. Also, the default constructor is required for exposed attributes in Spirit.
Now,
rule<It, identifier()> gr_identifier =
as_string [ (alpha | '_') >> *(alnum | '_') ]; // cleaner... but no fusion
works. This might be more intuitive if you didn't need Fusion of the type for other purposes.
Note: this variant could well be the most efficient in compile-time
Summary
I think for your code, there are 2 perfectly viable workarounds (#1 and #3), and one less-than-stellar (the one with the dummy field), but I included it for documentary purposes.
Full Code
For future reference
#define BOOST_SPIRIT_DEBUG
#include <string>
#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/include/adapt_struct.hpp>
namespace qi = boost::spirit::qi;
//////////////////////////////////////////
// Select workaround to demonstrate
#define KEEP_STRING_WORKAROUND
// #define DUMMY_WORKAROUND™
// #define NO_ADAPT_WORKAROUND
//////////////////////////////////////////
#if defined(KEEP_STRING_WORKAROUND)
struct identifier
{
std::string name;
};
BOOST_FUSION_ADAPT_STRUCT(
identifier,
(std::string, name)
)
#elif defined(DUMMY_WORKAROUND)
struct identifier
{
std::string name;
qi::unused_type dummy;
};
BOOST_FUSION_ADAPT_STRUCT(
identifier,
(std::string, name)
(qi::unused_type, dummy)
)
#elif defined(NO_ADAPT_WORKAROUND)
struct identifier
{
std::string name;
identifier() = default;
explicit identifier(std::string name)
: name(std::move(name))
{}
};
#endif
struct problem
{
identifier _1;
identifier _2;
identifier _3;
};
BOOST_FUSION_ADAPT_STRUCT(
problem,
(identifier, _1)
(identifier, _2)
(identifier, _3)
)
//////////////////////////////////////////
// For BOOST_SPIRIT_DEBUG only:
static inline std::ostream& operator<<(std::ostream& os, identifier const& id) {
return os << id.name;
}
//////////////////////////////////////////
int main()
{
using namespace qi;
typedef std::string::const_iterator It;
#if defined(KEEP_STRING_WORKAROUND)
rule<It, std::string()> gr_identifier =
(alpha | '_') >> *(alnum | '_');
#elif defined(DUMMY_WORKAROUND)
rule<It, identifier()> gr_identifier =
(alpha | '_') >> *(alnum | '_') >> attr(42); // that's hacky
#elif defined(NO_ADAPT_WORKAROUND)
rule<It, identifier()> gr_identifier =
as_string [ (alpha | '_') >> *(alnum | '_') ]; // cleaner... but no fusion
#endif
rule<It, problem(), qi::space_type> gr_problem =
gr_identifier
>> gr_identifier
>> ('(' > gr_identifier > ')')
;
std::string input = "foo goo(hoo)";
BOOST_SPIRIT_DEBUG_NODES((gr_problem)(gr_identifier));
It f(begin(input)), l(end(input));
bool dummy = phrase_parse(f, l, gr_problem, qi::space);
return dummy? 0 : 255;
}
[1] Believe me, I tried, even while inserting qi::unused_type
"fake" attributes, and/or using attr_cast<>
or helper rules to coerce the type of the subexpression.
[2] For demo purposes, I used std::string
because I believe it mixes better with BOOST_SPIRIT_DEBUG