Search code examples
c++boostboost-spirit

Boost Karma for a boost::variant containing a custom class


I'd like to investigate boost::spirit::karma::generate as a replacement for std::stringstream for a boost::variant containing, apart from familiar types such as int and double, one or more custom classes (e.g. A). However I'm unable to even compile the code once I include one or more custom classes in the variant.

#include <iostream>
#include <boost/spirit/include/karma.hpp>
#include <boost/variant.hpp>

struct A {
    double d;
    explicit A(const double d) : d(d) {}

    friend std::ostream& operator<<(std::ostream& os, A const& m) {
        return os << "{" << m.d << "}";
    }
};

BOOST_FUSION_ADAPT_STRUCT(A, d)

using Variant = boost::variant<int, double, A>;

void test_stringstream(const Variant &v) {
    std::stringstream os;
    os << v;
    std::cout << os.str() << std::endl;
}

void test_karma(const Variant &v) {
    std::string str;
    boost::spirit::karma::generate(std::back_inserter(str), v);
    std::cout << str << std::endl;
}

int main() {
    A a(double(1.0));
    std::cout << a << std::endl;
    test_stringstream(Variant(a));
    test_karma(Variant(a));
}

I'd expect the output:

{1}
{1}
{1}

The error output begins with

mpl_iterator.hpp:45:24: error: no matching constructor for initialization of 'boost::fusion::mpl_iterator<boost::mpl::l_iter<boost::mpl::l_item<mpl_::long_<1>, A, boost::mpl::l_end>>>::deref<boost::fusion::mpl_iterator<boost::mpl::l_iter<boost::mpl::l_item<mpl_::long_<1>, A, boost::mpl::l_end>>>>::type' (aka 'A')
                return type();

Apart from custom classes, I'd also like to enquire how one could make Enums 'streamable' as it were via boost::spirit::karma::generate.


Solution

  • The error message informs you that A is not default-constructible. It's a long type expression, but helpfully summarized it for you: (aka 'A').

    Adding a default value for d fixes it:

    explicit A(double d = {}) : d(d) {}
    

    Now with

    std::cout << "iostream:     " << a << std::endl;
    std::cout << "stringstream: "; test_stringstream(Variant(a));
    std::cout << "karma:        "; test_karma(Variant(a));
    

    You'd see (Live)

    iostream:     {1}
    stringstream: {1}
    karma:
    

    You didn't pass any karma expression to generate. Let's add:

    void test_karma(const Variant& v) {
        namespace k = boost::spirit::karma;
        std::string str;
        k::generate(std::back_inserter(str), k::stream, v);
        std::cout << str << std::endl;
    }
    

    Now you get the same output. Note that the Fusion adaptation is completely unnecessary here, don't actually ever deal with A in your karma expression. It will effectively amount to the stringstream implementation, but with extra steps, so it will be slower.

    Demo

    Live

    Prints

    iostream:     {1}
    stringstream: {1}
    karma:        {1}
    

    Suggestions

    I'd suggest using Boost lexical_cast here. It's almost guaranteed to be faster and certainly less ... clumsy:

    Live On Coliru

    #include <boost/lexical_cast.hpp>
    #include <boost/variant.hpp>
    #include <iostream>
    
    struct A {
        explicit A(double d = {}) : d(d) {}
    
      private:
        double d;
        friend std::ostream& operator<<(std::ostream& os, A const& m) {
            return os << "{" << m.d << "}";
        }
    };
    
    using Variant = boost::variant<int, double, A>;
    
    int main() {
        Variant vv[]{42, 3.14, A(1.0)};
        for (Variant a : vv)
            std::cout << boost::lexical_cast<std::string>(a) << "\n";
    }
    

    Prints

    42
    3.14
    {1}