Search code examples
c++boostboost-spiritboost-spirit-karma

Boost Karma: writing a rule with boost::optional<boost::variant<...>>


I am attempting to use a boost::variant within a boost::optional in a Karma generator. I've been able to reduce the problem to this:

using FooVariant = boost::variant<int>;
using FooOptional = boost::optional<FooVariant>;

template<typename OutputIt = boost::spirit::ostream_iterator>
struct FooGenerator
    : boost::spirit::karma::grammar<OutputIt, FooOptional()>
{
    FooGenerator()
        : FooGenerator::base_type(start_)
    {
        namespace bsk = boost::spirit::karma;
        start_ = '[' << ( bsk::int_ | '*' ) << ']';
    }

    boost::spirit::karma::rule<OutputIt, FooOptional()> start_;
};

int main()
{
    FooOptional fop1;
    std::cout << boost::spirit::karma::format(FooGenerator<>(), fop1) << std::endl;

    FooOptional fop2 = FooVariant{123};
    std::cout << boost::spirit::karma::format(FooGenerator<>(), fop2) << std::endl;
}

The output here is:

[*]
[*]

Which is not what I was hoping for.

One of the first things I changed was more or less a sanity check by changing FooVariant to: using FooVariant = int;. This outputs:

[*]
[123]

And is what I want to see! So in my first code, the variant had only one type, so I tried adding a second type, just to see:

using FooVariant = boost::variant<int, double>;
...
start_ = '[' << ( ( bsk::int_ | bsk::double_ ) | '*' ) << ']';

But then we're back to:

[*]
[*]

Then I tried to add a rule specifically for the variant:

using FooVariant = boost::variant<int, double>;
using FooOptional = boost::optional<FooVariant>;

template<typename OutputIt = boost::spirit::ostream_iterator>
struct FooGenerator
    : boost::spirit::karma::grammar<OutputIt, FooOptional()>
{
    FooGenerator()
        : FooGenerator::base_type(start_)
    {
        namespace bsk = boost::spirit::karma;
        foovar_ = (bsk::int_ | bsk::double_);
        start_ = '[' << ( foovar_ | '*' ) << ']';
    }

    boost::spirit::karma::rule<OutputIt, FooVariant()> foovar_;
    boost::spirit::karma::rule<OutputIt, FooOptional()> start_;
};

int main()
{
    FooOptional fop1;
    std::cout << boost::spirit::karma::format(FooGenerator<>(), fop1) << std::endl;

    FooOptional fop2 = FooVariant{123};
    std::cout << boost::spirit::karma::format(FooGenerator<>(), fop2) << std::endl;
}

Which gives a compilation error:

alternative_function.hpp:127:34: error: no member named 'is_compatible' in
      'boost::spirit::traits::compute_compatible_component<boost::variant<int, double>, boost::optional<boost::variant<int, double> >, boost::spirit::karma::domain>'
            if (!component_type::is_compatible(spirit::traits::which(attr_)))
                 ~~~~~~~~~~~~~~~~^

It looks like the template generation is trying to make sure that the boost::variant and boost::optional are compatible but for me the question is "why is it trying to make sure they're compatible at all?"

How can I make this work?


Solution

  • I told you in the other answer how I'd handle this: Boost Karma: generate default text when boost::optional is unset

    Not only does that sidestep the issue, it also simplifies the AST/data types.

    Now, since you're forcing me¹, I dug in. The problem arises from the fact that foovar_ | '|' is an alternative-expression and somehow it validates that the attribute must be compatible. During that check there is the assumption that

    • If a has attribute attr(a) and b has attribute attr(b) then
    • a | b should have variant<attr(a), attr(b)

    It does do some logic checks (like when attr(a) == attr(b)) but not the logic check that if attr(b) is unused_type then it should be compatible with optional<attr(a)> too instead of just with variant<attr(a), unused_type> or even variant<attr(a)>.

    Now, the why doesn't interest me that much². So, here's how you can force things.

        foovar_ = bsk::int_ | bsk::double_;
        fooopt_ = (bsk::eps(is_initialized_(bsk::_val)) | '*') << -foovar_;
        start_ = '[' << fooopt_ << ']';
    

    The key is to encode it as something other than an alternative-expression. Here we simply trust -foovar_ to say something, or not. In addition, we also do

    bsk::eps(is_initialized_(bsk::_val)) | '*'
    

    meaning: If the variant is initialized, that's all, otherwise, generate '*'.

    Now you don't strictly need the third rule, if you like write-only code, you can just write:

        start_ = '[' << (bsk::eps(is_initialized_(bsk::_val)) | '*') << -foovar_ << ']';
    

    DEMO

    Oh, I almost forget:

    struct is_initialized_f {
        template<typename T>
        bool operator()(boost::optional<T> const& opt) const { return opt.is_initialized(); }
    };
    boost::phoenix::function<is_initialized_f> is_initialized_;
    

    Implements that helper Phoenix actor to check the initialization status.

    Live On Coliru

    #include <boost/spirit/include/karma.hpp>
    #include <boost/spirit/include/phoenix.hpp>
    using FooVariant = boost::variant<int, double>;
    using FooOptional = boost::optional<FooVariant>;
    
    template<typename OutputIt = boost::spirit::ostream_iterator>
    struct FooGenerator
        : boost::spirit::karma::grammar<OutputIt, FooOptional()>
    {
        FooGenerator()
            : FooGenerator::base_type(start_)
        {
            namespace bsk = boost::spirit::karma;
            namespace phx = boost::phoenix;
    
            foovar_ = bsk::int_ | bsk::double_;
            //fooopt_ = (bsk::eps(is_initialized_(bsk::_val)) | '*') << -foovar_;
            start_ = '[' << (bsk::eps(is_initialized_(bsk::_val)) | '*') << -foovar_ << ']';
        }
    
      private:
        struct is_initialized_f {
            template<typename T>
            bool operator()(boost::optional<T> const& opt) const { return opt.is_initialized(); }
        };
        boost::phoenix::function<is_initialized_f> is_initialized_;
        boost::spirit::karma::rule<OutputIt, FooVariant()> foovar_;
      //boost::spirit::karma::rule<OutputIt, FooOptional()> fooopt_;
        boost::spirit::karma::rule<OutputIt, FooOptional()> start_;
    };
    
    int main()
    {
        for (FooOptional fop : { FooOptional{}, {FooVariant{123}}, {FooVariant{3.14}} }) {
            if (std::cout << boost::spirit::karma::format(FooGenerator<>(), fop))
                std::cout << "\n";
            else
            {
                std::cout.clear();
                std::cout << "#Error\n";
            }
        }
    }
    

    Prints

    [*]
    [123]
    [3.14]
    

    ¹ joke

    ² we'll get non-authoritative answers that rhyme with "someone forgot" or "it's a regression"