Nico Schlömer
Nico Schlömer

Reputation: 58881

cout map with boost::any

I have a "dictionary" std::map<std::string, boost::any> (or std::any, if you want) that can possibly be nested. Now, I would like to display the map. Since boost::any obviously doesn't play nicely with <<, things are getting a little nasty. So far, I'm checking the type, cast it, and pipe the cast to cout:

for (const auto &p: map) {
  std::cout << std::string(indent + 2, ' ') << p.first << ": ";
  if (p.second.type() == typeid(int)) {
    std::cout << boost::any_cast<int>(p.second);
  } else if (p.second.type() == typeid(double)) {
    std::cout << boost::any_cast<double>(p.second);
  } else if (p.second.type() == typeid(std::string)) {
    std::cout << boost::any_cast<std::string>(p.second);
  } else if (p.second.type() == typeid(const char*)) {
    std::cout << boost::any_cast<const char*>(p.second);
  } else if (p.second.type() == typeid(std::map<std::string, boost::any>)) {
    show_map(
        boost::any_cast<std::map<std::string, boost::any>>(p.second),
        indent + 2
        );
  } else {
    std::cout << "[unhandled type]";
  }
  std::cout << std::endl;
}
std::cout << std::string(indent, ' ') << "}";

This prints, for example

{
  fruit: banana
  taste: {
    sweet: 1.0
    bitter: 0.1
  }
}

Unfortunately, this is hardly scalable. I'd have to add another else if clause for every type (e.g., float, size_t,...), which is why I'm not particularly happy with the solution.

Is there a way to generalize the above to more types?

Upvotes: 2

Views: 2261

Answers (2)

manlio
manlio

Reputation: 18962

Since you're already using Boost you could consider boost::spirit::hold_any.

It already has pre-defined streaming operators (both operator<<() and operator>>()).

Just the embedded type must have the corresponding operator defined, but in your use context this seems to be completely safe.

Despite being in the detail namespace, hold_any is quite widespread and almost a ready-to-use boost:any substitute (e.g. Type Erasure - Part IV, Why you shouldn’t use boost::any)

A recent version of Boost is required (old versions had a broken copy assignment operator).

Upvotes: 1

Tony Delroy
Tony Delroy

Reputation: 106236

One thing you can do to lessen (but not remove) the pain is to factor the type determination logic into one support function, while using static polymorphism (specifically templates) for the action to be applied to the values...

#include <iostream>
#include <boost/any.hpp>
#include <string>

struct Printer
{
    std::ostream& os_;

    template <typename T>
    void operator()(const T& t)
    {
        os_ << t;
    }
};

template <typename F>
void f_any(F& f, const boost::any& a)
{
    if (auto p = boost::any_cast<std::string>(&a)) f(*p);
    if (auto p = boost::any_cast<double>(&a))      f(*p);
    if (auto p = boost::any_cast<int>(&a))         f(*p);
    // whatever handling for unknown types...
}

int main()
{
    boost::any anys[] = { std::string("hi"), 3.14159, 27 };
    Printer printer{std::cout};
    for (const auto& a : anys)
    {
        f_any(printer, a);
        std::cout << '\n';
    }
}

(With only a smidge more effort, you could have the type-specific test and dispatch done for each type in a variadic template parameter pack, simplifying that code and the hassle of maintaining the list. Or, you could just use a preprocessor macro to churn out the if-cast/dispatch statements....)

Still - if you know the set of types, a boost::variant is more appropriate and already supports similar operations (see here).

Yet another option is to "memorise" how to do specific operations - such as printing - when you create your types:

#include <iostream>
#include <boost/any.hpp>
#include <string>
#include <functional>

struct Super_Any : boost::any
{
    template <typename T>
    Super_Any(const T& t)
      : boost::any(t),
        printer_([](std::ostream& os, const boost::any& a) { os << boost::any_cast<const T&>(a); })
    { }

    std::function<void(std::ostream&, const boost::any&)> printer_;
};

int main()
{
    Super_Any anys[] = { std::string("hi"), 3.14159, 27 };
    for (const auto& a : anys)
    {
        a.printer_(std::cout, a);
        std::cout << '\n';
    }
}

If you have many operations and want to reduce memory usage, you can have the templated constructor create and store a (abstract-base-class) pointer to a static-type-specific class deriving from an abstract interface with the operations you want to support: that way you're only adding one pointer per Super_Any object.

Upvotes: 3

Related Questions