std::any vs. std::variant (3)

As promised, in this article we’re going to look at using any to store a representation of an arbitrary JSON object. In case you didn’t already know, JSON stands for “JavaScript Object Notation” and is easy to parse both for humans and machines (compared to XML, for example). A JSON object is essentially an array of key names followed by a colon followed by the value, separated by commas and surrounded by curly braces. So already we can name our container type for JSON:

using JSON = vector<pair<string,any>>;

Easy! Even at this early stage, we can populate the JSON object (but of course we can’t output it with the stream insertion operator (<<) as this hasn’t been defined):

int main() {
    JSON obj;
    obj.emplace_back("a", JSON{});
    obj.emplace_back("b", any{});
    obj.emplace_back("c", 1.2);
    obj.emplace_back("d", "Hi!"s);
    cout << obj << '\n';           // this line won't compile at this stage
}

(In case you’re wondering, the parameters to emplace_back() are forwarded to the constructor for the vector‘s element type, in this case pair<string,any>; this makes it slightly more efficient than push_back() which would take a pair temporary as its single argument.)

The values used equate to: a) empty JSON object (this is legal as JSON allows nesting to arbitrary depth), b) special JSON type undefined, c) a double, and d) a std::string. If we want to be able to print these out, we must somehow handle them individually. Here are the beginnings of a suitable (global) operator<< function:

ostream& operator<< (ostream& os, const JSON& json) {
    if (json.empty()) {
        return os << "{}";
    }
    auto sep = ""sv;
    os << "{ ";
    for (const auto& elem : json) {
        os << sep;
        if (elem.first.find(" ") != string::npos) {
            os << '\'' << elem.first << "\': ";
        }
        else {
            os << elem.first << ": ";
        }
        if (elem.second.type() == typeid(void)) {
            os << "undefined";
        }
        else if (elem.second.type() == typeid(double)) {
            os << any_cast<double>(elem.second);
        }
        else if (elem.second.type() == typeid(string)) {
            os << '\'' << any_cast<string>(elem.second) << '\'';
        }
        else if (elem.second.type() == typeid(JSON)) {
            os << any_cast<JSON>(elem.second);
        }
        sep = ", "sv;
    }
    return os << " }";
}

This code is not particularly complex as long as you’re familiar with vector and pair, and followed the use of comparing typeid(T) with any‘s type() member function:

  • Line 2 checks the parameter json‘s member function empty(), this is fine as json is “just” a vector so its member functions can be used in the usual way. This check avoids spaces in the output if the JSON object is empty.
  • Line 5 sets the variable sep which then has the value ", " on exit from the first iteration of the range-for loop, this is the standard trick for outputting separated entities when using range-for.
  • Line 7 begins the range-for iterating over the vector with the current element named elem; this element has type pair<string,any> with fields called first and second.
  • Line 9 checks the element key name for any space characters, if there are then this name is outputted with enclosing single quotes. A colon follows in either case.
  • Lines 15-26 are chained if-else-if clauses which check the element value type against all the ones we need to handle. The order is unimportant; note the use of any_cast in every clause. In production code we would probably want to throw an exception if no match is made; this could be achieved by a final else clause.
  • Line 29 returns the modified std::ostream& in the same way as line 3.

This code is far from polished, but manages to sketch the outline of a method. The output from putting this code together (view full source here) is:

{ a: {}, b: undefined, c: 1.2, d: 'Hi!' }

As an easy exercise you could try to add support for ints and bools, a slightly harder one could be adding support for JSON arrays (which are delimited with square brackets), while a real challenge would be overloading the stream extraction operator (>>). Hint: for the second of these use vector<any> and if you get stuck then look here.

That’s all for now, in the next article in this mini-series we’ll look at attempting to replicate this functionality using variant instead of any.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s