c++boostboost-spiritboost-spirit-karma

boost::spirit::karma alternative generator with boost::variant consisting a string and string alias


I have a boost::variant consisting out of several types, including string type aliases and a string type. The string type aliases work as aspected with the boost::spirit::qi alternative parser, but the boost::spirit::karma alternative generator does not only work in a unwanted but also unexpected way, by not using the wanted string type alias generator rule, but also by not even using the built in string generator, when the variant includes the string type:

#include <iostream>
#include <iterator>
#include <string>
#include <vector>
#include <boost/spirit/include/karma.hpp>

using mode = std::string;
using alt_variant = boost::variant<mode, std::string, unsigned>;
using alt_variant_without_string = boost::variant<mode, unsigned>;

template <typename OutputIterator>
boost::spirit::karma::rule<OutputIterator, mode()>
        mode_gen{
        boost::spirit::karma::lit("mode=\"") <<
                                             boost::spirit::karma::string
                                             << boost::spirit::karma::lit("\"")
};

int main(int argc, char *argv[]) {
    alt_variant foo1{mode{"bar"}};
    alt_variant_without_string foo2{mode{"bar"}};
    std::string output;
    using namespace boost::spirit::karma;
    const auto gen = mode_gen<std::back_insert_iterator<std::string>> | uint_ | string;

    boost::spirit::karma::generate(std::back_inserter(output), gen, foo1);
    std::cout << "Output\"" << output << "\"\n"; //Output""

    output.clear();

    boost::spirit::karma::generate(std::back_inserter(output), gen, foo2);
    std::cout << "Output\"" << output << "\"\n";//Output"mode="bar""

    return 0;
}

Can somebody explain this behaviour, and how I get the wanted behaviour?

For the later one I guess, I have to get rid of all string type aliases and use explicit structs as types, but then I fall again in the ugly one member struct corner case. ( https://codereview.stackexchange.com/q/206259/95143 However, that the first output is not at least just "bar" i.e. that the string generator isn't used when the mode generator isn't either, looks like a bug to me i.e. I can't understand.


Solution

  • Where to start.

    A. Unspecified Behaviour

    This might actually be Undefined Behaviour but I didn't check the documentation.

    A type alias does not create a new type. Therefore typeid(std::string) == typeid(mode) and there is no way the variant can distinguish the two element types.

    The behaviour of Variant is unspecified. Compare: Live On Coliru

    boost::variant<mode, std::string> v;
    

    And Live On Coliru

    boost::variant<int, mode, std::string> v;
    

    B. Undefined Behaviour

    And then you do

    const auto gen = mode_gen<std::back_insert_iterator<std::string> > | uint_ | string;
    

    Same applies as with Qi: the proto-expressions hold rule operands by reference, and that means auto is a bad idea:

    Run your code with UBSan/ASan and use Valgring to catch errors like these, before they eat your customer's data.

    The Problem

    Your problem is you want expressive types that you can switch on. I think Java-ists like to call it Abstract Data Types. It's a lofty goal, and you can:

    Solution 1

    Make mode a custom type:

    Live On Coliru

    #include <boost/spirit/include/karma.hpp>
    #include <iostream>
    #include <iterator>
    #include <string>
    #include <vector>
    
    struct mode : std::string {
        using std::string::string;
    };
    
    namespace karma = boost::spirit::karma;
    
    template <typename Out = boost::spirit::ostream_iterator>
    karma::rule<Out, mode()> mode_gen = "mode=\"" << karma::string << "\"";
    
    int main() {
        using Variant = boost::variant<mode, std::string, unsigned>;
    
        Variant foo = std::string("foo"),
                bar = mode("bar"),
                i = 42;
    
        for (Variant v : { foo, bar, i })
            std::cout << "Output: " << format(mode_gen<> | karma::uint_ | karma::string, v) << "\n";
    }
    

    Prints

    Output: foo
    Output: mode="bar"
    Output: 42
    

    Solution #2: Strong Typedef

    I couldn't make this work right away, so let me just point at a sample implementation:

    #include <boost/serialization/strong_typedef.hpp>
    

    Solution #3: Distinguishing std::string

    You can use a hack:

    namespace hack {
        template <typename Char, typename Tag>
        struct my_traits : std::char_traits<Char> {};
    }
    
    using mode = std::basic_string<char, hack::my_traits<char, struct ModeTag> >;
    

    That still prints the same Live On Coliru

    Output: foo
    Output: mode="bar"
    Output: 42
    

    BONUS

    There are issues with your generator. Specifically, if your mode value contains a quote, things will go awry. You might simply leverage ostream:

    struct mode : std::string {
        using std::string::string;
    
        friend std::ostream& operator<<(std::ostream& os, mode const& m) {
            return os << "mode=" << std::quoted(m);
        }
    };
    

    This way a simple

    std::cout << mode("yo") << std::endl;
    std::cout << mode("y\"!\"o") << std::endl;
    

    would print Live On Coliru

    mode="yo"
    mode="y\"!\"o"
    

    Which is considerably more elegant. It also means you can replace all of the karma grammar with karma::stream:

    Live On Coliru

    #include <boost/spirit/include/karma.hpp>
    #include <iostream>
    #include <iomanip>
    
    struct mode : std::string {
        using std::string::string;
    
        friend std::ostream& operator<<(std::ostream& os, mode const& m) {
            return os << "mode=" << std::quoted(m);
        }
    };
    
    int main() {
        boost::variant<mode, std::string, unsigned> 
            foo = std::string("foo"),
            bar = mode("bar"),
            i = 42;
    
        for (auto v : { foo, bar, i })
            std::cout << "Output: " << karma::format(karma::stream, v) << "\n";
    }
    

    I LOVE IT when less and less code does more and more. But at this rate, one wonders why even use karma?

    BONUS #2 - ADL It, and who needs Karma

    To make it shine with the my_traits approach and your Tag type, take Argument Dependent Lookup to the max:

    Live On Coliru

    #include <boost/variant.hpp>
    #include <iostream>
    #include <iomanip>
    
    namespace hack {
        template <typename Char, typename Tag>
        struct my_traits : std::char_traits<Char> {};
    }
    
    namespace mylib {
        struct ModeTag{};
        struct ValueTag{};
    
        static inline std::ostream& operator<<(std::ostream& os, ModeTag)  { return os << "mode"; }
        static inline std::ostream& operator<<(std::ostream& os, ValueTag) { return os << "value"; }
    
        template <typename Char, typename Tag>
        static inline std::ostream& operator<<(std::ostream& os, hack::my_traits<Char, Tag>)
            { return os << Tag{}; }
    
        template <typename Char, typename CharT, typename Alloc>
        std::ostream& operator<<(std::ostream& os, std::basic_string<Char, CharT, Alloc> const& s) {
            return os << CharT{} << "=" << std::quoted(s);
        }
    }
    
    using mode = std::basic_string<char, hack::my_traits<char, struct mylib::ModeTag> >;
    using value = std::basic_string<char, hack::my_traits<char, struct mylib::ValueTag> >;
    
    int main() {
        boost::variant<mode, value, unsigned> 
            foo = value("foo"),
            bar = mode("bar"),
            i = 42;
    
        std::cout << foo << std::endl;
        std::cout << bar << std::endl;
        std::cout << i << std::endl;
    }
    

    It compiles 10x faster and prints:

    value="foo"
    mode="bar"
    42