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 thevariant
object, and we switch on itsindex()
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 thevariant
object withget<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 int
s and bool
s 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