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

In this fourth and final article of this mini-series we’re going to look at replicating the JSON container program from part three using variant instead of any. As previously mentioned variant is essentially a type-safe union with an additional discriminant field, which means that extracting the correct type is less problematic than for any. Essentially, it is possible to switch on the index() member function, which returns the index of the type that the variant object holds, instead of requiring chained if-else-if statements.

There is a slight complication with variant in that it can’t directly reference its own type, unlike any; this matters because JSON objects can contain other JSON objects as values. Therefore the following code is malformed:

using JSON = vector<pair<string,variant<monostate,double,string,JSON>>>; // does not compile

And so, unfortunately, is this:

using JSON = vector<pair<string,variant<monostate,double,string,unique_ptr<JSON>>>>; // also does not compile

However the following code is fine (using a std::unique_ptr to a type that has been only declared, not defined):

struct JSON_s;
using JSON = vector<pair<string,variant<monostate,double,string,unique_ptr<JSON_s>>>>;
struct JSON_s {
    JSON data;
};

Of course, this adds complications to both constructing the unique_ptr<JSON_s> and dereferencing the data member when outputting. Here is a first attempt at operator <<:

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 << ": ";
        }
        switch (elem.second.index()) {
            case 0:
                os << "undefined";
                break;
            case 1:
                os << get<double>(elem.second);
                break;
            case 2:
                os << '\'' << get<string>(elem.second) << '\'';
                break;
            case 3:
                os << get<unique_ptr<JSON_s>>(elem.second)->data;
                break;
        }
        sep = ", ";
    }
    return os << " }";
}
  • This code is essentially the same as using vector<pair<string,any>> up to the switch statement at line 15.
  • The member elem.second is the variant object, and we switch on its index() member function.
  • The code for cases 0, 1, and 2 mirror those for the previous version which used any.
  • Line 26 may be quite hard to decipher; we have to extract a unique_ptr from the variant object with get<unique_ptr<JSON_s>>(elem.second) and then dereference this to obtain the actual member using pointer-notation as ->data, which is then passed to the stream.

Now for the main() function, which contains a couple of changes from the previous version for fields a and b:

int main() {
    JSON obj;
    obj.emplace_back("a", make_unique<JSON_s>());
    obj.emplace_back("b", monostate{});
    obj.emplace_back("c", 1.2);
    obj.emplace_back("d", "Hi!"s);
    cout << obj << '\n';
}

The output, however remains identical (complete program here):

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

As before, the effort of adding support for ints and bools is small, although the case statements must match the order of the template type parameters where the variant is defined; you will likely get std::bad_variant_access exceptions thrown if they don’t. Adding support for arrays is possible, needing another unique_ptr<JSONArray_s> (or similar) field to the variant definition.

So which is better? That’s a difficult question to answer: personally I think the version using any creates cleaner and simpler code, but the version using variant does not rely so heavily on heap access and may be more time- and memory-efficient (especially if you used your own string class that contained just a pointer and length, or even unique_ptr<char[]>). Most importantly we’ve demonstrated that this task can be capably performed with either – competitive coding anyone?

That’s all for this mini-series. If you do decide to implement JSON arrays for the variant version, or operator >> for either, then please do share and leave a comment with a link. Thanks!

Resources: Download or browse the source code

Leave a comment