2

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.

Superlokkus
  • 4,731
  • 1
  • 25
  • 57
  • Searching for the flavoured string reference made me found this similar answer https://stackoverflow.com/a/26038867/85371 – sehe Oct 25 '18 at 23:02

1 Answers1

3

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


sehe
  • 374,641
  • 47
  • 450
  • 633
  • Credit to @R.MartinhoFernandes for coming up with the original idea for flavoured strings 6 years ago: https://gist.github.com/rmartinho/5227803 – sehe Oct 25 '18 at 22:59
  • 1
    Thank you very very much, too bad I can only upvote it once ;-) . I knew when I wrote all those type aliases than it might brake, but it worked fine in the beginning, but thats the thing about native, UB might work sometimes. Thanks also for the alternatives. But I will stick to karma, since this in a minimalized example from the RTSP transport header generation, but I will keep your bonus in mind, since I wrote some code before where I want the types be ostream outable. And your code might be slimmer. – Superlokkus Oct 26 '18 at 21:33
  • However Solution #2 has some ramifications, when I use it for my other types, it throws some template errors, deep in boost, I guess one can't use the boost strong typedef directly, but I guess can take it as a guideline, but Solution#1 works already fine :-) boost/mpl/not.hpp:41/opt/local/include/boost/type_traits/has_nothrow_constructor.' error: exception specification is not available until end of class definition BOOST_MPL_AUX_NESTED_TYPE_WKND(T)::value – Superlokkus Oct 26 '18 at 21:47