1

Been playing with Boost:make_recursive_variant and I'm quite stump on how to create a string from a given Variant and return it.

I can output easily using cout, but my goal was to create a C++ Version of Arrays.deeptoString from java to return a string. Kept running into compile issues trying to work around recursive_variant. Here my Current Code for Arrays.deeptoString.

typedef boost::make_recursive_variant<string, int, vector<boost::recursive_variant_ > >::type ObjectE;
class deepToString : public boost::static_visitor<std::string> {
public:
    string operator()(const int i) const {
        storedString += i;
        storedString += ",";
        return storedString;
    }
    string operator()(std::vector<ObjectE> const &v) const {
        storedString += "[";
        for (std::size_t i = 0; i < v.size() - 1; i++) {
            storedString += boost::apply_visitor(create_string(), v[i]);
            storedString += ",";
        }
        storedString += boost::apply_visitor(create_string(), v[v.size() - 1]);
        storedString += "]";
        return storedString;
    }
    string operator()(std::string const &s) const {
        storedString += s;
        storedString += ",";
    }
    mutable string storedString = "";
};

I was hoping I could return the string, but instead it just crashes on me. I... both love and hate recursive variants atm. Ideally if there was a given

ObjectE t = ["string" , [1, 2] , ["str2", "str3"], [1,3,6], [[1,2], [1,4]]]

I should be able to do the following line to get

string output = boost::apply_visitor(deepToString(), t);

output get

 "["string" , [1, 2] , ["str2", "str3"], [1,3,6], [[1,2], [1,4]]]"

At the start, I tried not using const methods since we are changing the values inside the method, but I got a large series of compiles errors. This current style compiles for me, but will crash on me. I should have prob look into why it crashing first.

Any ideas on how I should approach this problem

Leruce
  • 187
  • 1
  • 8

1 Answers1

4

There are a number of problems:

  1. doing storedString += i; (where i is int) doesn't do what you think it does
  2. You fail to return a value from operator()(std::string const&) const
  3. You recurse into create_string() which is a temporary; this is the reason you couldn't make things const properly

Fixing all that and doing it right with the mutable/const:

Live On Coliru

#include <boost/variant.hpp>
#include <vector>
#include <iostream>

typedef boost::make_recursive_variant<std::string, int, std::vector<boost::recursive_variant_> >::type ObjectE;
typedef std::vector<ObjectE> ObjectV;

struct create_string : boost::static_visitor<std::string const&> {
    std::string const& operator()(const int i) {
        return storedString += std::to_string(i) + ", ";
    }
    std::string const& operator()(ObjectV const &v) {
        storedString += "[ ";
        for (std::size_t i = 0; i < v.size() - 1; i++) {
            create_string nest;
            storedString += boost::apply_visitor(nest, v[i]) + ", ";
        }
        create_string nest;
        storedString += boost::apply_visitor(nest, v[v.size() - 1]);
        return storedString += " ]";
    }
    std::string const& operator()(std::string const &s) {
        return storedString += s + ", ";
    }

    std::string storedString = "";
};

int main() {
    ObjectE obj = ObjectV { 
        1,
        "hello world",
        ObjectV {
            42, ObjectV { "some more" }, -42
        },
    };

    create_string vis;

    std::cout << "Result '" << boost::apply_visitor(vis, obj) << "'\n";
}

Prints:

Result '[ 1, , hello world, , [ 42, , [ some more,  ], -42,  ] ]'

Improving

You can do a lot better.

  • First you can reduce allocations by using a stream instead of copying strings. This could also be made to make the visitor stateless.

  • You can also make the visitor apply directly to variants, so you don't have to invoke apply_visitor anymore.

  • You can avoid the redundant commas and use std::quoted to properly escape the string values (in case they contain ", [, ], ,)

  • You can make everything generic so it works with a lot more variants

Here's the implementation of these bullets:

Live On Coliru

#include <boost/variant.hpp>
#include <vector>
#include <iostream>
#include <iomanip>

typedef boost::make_recursive_variant<std::string, int, std::vector<boost::recursive_variant_> >::type ObjectE;
typedef std::vector<ObjectE> ObjectV;

struct printer {
    using result_type = void;
    std::ostream& _os;

    // forwards for `operator()`
    template <typename T> void call(T const& v) const { return operator()(v); }

    // dispatch for variants
    template <typename... Ts> void operator()(boost::variant<Ts...> const& v) const {
        return boost::apply_visitor(*this, v);
    }

    void operator()(int i) const                { _os << i;              } 
    void operator()(std::string const &s) const { _os << std::quoted(s); } 

    template <typename... Ts> void operator()(std::vector<Ts...> const& v) const {
        _os << "[ ";
        bool first = true;
        for (auto& el : v) {
            if (first) first = false;
            else       _os << ", ";
            call(el);
        }
        _os << " ]";
    }
};

int main() {
    ObjectE obj = ObjectV { 
        1,
        "hello world",
        ObjectV {
            42, ObjectV { "some more" }, -42
        },
    };

    printer print{std::cout};
    print(obj);
}

Printing

[ 1, "hello world", [ 42, [ "some more" ], -42 ] ]

Even More: Pretty Indenting

Now that we've lost the baggage of the string, we can add some state to the visitor again. Let's make it pretty print with indentation.

Live On Coliru

struct printer {
    using result_type = void;
    std::ostream& _os;
    std::string _indent = "";

    template <typename... Ts> void operator()(boost::variant<Ts...> const& v) const {
        return boost::apply_visitor(*this, v);
    }

    void operator()(int i) const                { _os << i;              } 
    void operator()(std::string const &s) const { _os << std::quoted(s); } 

    template <typename... Ts> void operator()(std::vector<Ts...> const& v) const {
        _os << "[";
        auto indent = _indent + "    ";

        bool first = true;
        for (auto& el : v) {
            if (first) first = false;
            else _os << ",";
            _os << "\n" << indent;
            printer{_os, indent}(el);
        }

        if (!v.empty())
            _os << "\n" << _indent;
        _os << "]";
    }
};

Prints

[
    1,
    "hello world",
    [],
    [
        42,
        [
            "some more"
        ],
        -42
    ]
]

For The Win: Without Recursive Variant, Neat Literals

I noticed you wrote your pseudo-code more like JSON:

ObjectE t = ["string" , [1, 2] , ["str2", "str3"], [1,3,6], [[1,2], [1,4]]]

That's not C++. However, I can make it work like this:

ObjectE t = {"string" , {1, 2} , {"str2", "str3"}, {1,3,6}, {{1,2}, {1,4}}};

You do so by making a type derived from variant, and adding a constructor that takes an initializer list for the vector element:

struct ObjectE : boost::variant<std::string, int, std::vector<ObjectE> > {
    using Base = boost::variant<std::string, int, std::vector<ObjectE> >;

    using Base::variant;
    using Base::operator=;

    ObjectE(std::initializer_list<ObjectE> init) : Base(std::vector<ObjectE>(init)) {}
};

That's all. The improved visitor above was generic enough from the start, so we don't need to change another line of code:

Live On Coliru

printer print{std::cout};
print(ObjectE { 1, "hello world", {}, { 42, { "some more" }, -42 } });

std::cout << "\nt: ";

ObjectE t = {"string" , {1, 2} , {"str2", "str3"}, {1,3,6}, {{1,2}, {1,4}}};
print(t);

Prints

[
    1,
    "hello world",
    [],
    [
        42,
        [
            "some more"
        ],
        -42
    ]
]
t: [
    "string",
    [
        1,
        2
    ],
    [
        "str2",
        "str3"
    ],
    [
        1,
        3,
        6
    ],
    [
        [
            1,
            2
        ],
        [
            1,
            4
        ]
    ]
]
sehe
  • 374,641
  • 47
  • 450
  • 633
  • I.. wasn't expecting such in depth answer. Will be reviewing them and applying what i learn from this with a another session. I was trying to use ostream before. Kept running into issues. Now I understand why. Thank for that tidbit. – Leruce Dec 07 '17 at 13:33
  • For the win, I added the final step that takes you to `print(ObjectE {"string" , {1, 2} , {"str2", "str3"}, {1,3,6}, {{1,2}, {1,4}}});` as your dream code had it :) **[Live On Coliru](http://coliru.stacked-crooked.com/a/639af8ee107f6b25)** – sehe Dec 07 '17 at 13:35
  • @Leruce "I.. wasn't expecting such in depth answer." We call that the sehe effect. – Ven Dec 07 '17 at 13:38
  • Hi Coliru, I am getting below error when putting elements of different type. My C++ version is bit old. – Chandan K Singh Apr 24 '19 at 11:37
  • @ChandanKSingh I asked the God of Coliru, but he doesn't know what you mean either. Did you forget to include a coliru link? – sehe Apr 25 '19 at 21:15