1. 11

  2. 4

    I stopped reading when I got to this bit:

    I did take a look at std::variant but it seemed like its API was overly complex.

    This was immediately after defining a problem for which std::variant is the optimal and idiomatic solution: a class that represents one out of a closed set of possible types. The std::variant API is not very complicated. There are basically two ways of using it. Assume that the class had been defined to contain:

      using JSONArray = std::vector<JSONValue>;
      using JSONObject = std::map<std::string, JSONValue>
      std::variant<std::string, double, bool, JSONArray, JSONObject> contents;

    This is keeping the same types from the original, though the choice of std::map is odd. std::map keeps the objects in order by the ordering defined by the comparator on the key. JSON objects typically should either be defined to be ordered by insertion order or unordered.

    First, you can just call std::holds_alternative to see if it olds a specific type and then std::get to get it. For example:

    if (std::holds_alternative<JSONObject(contents)>)
      auto &obj = std::get<JSONObject>(contents);
      // Do something with the object representation.

    The second option is to use std::visit, which lets you pattern match on the type, with code like this:

    std::visit([&](auto v) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, std::string>)
          // Do stuff with `v` as a string
        else if constexpr (std::is_same_v<T, double>)
          // Do stuff with `v` as a double
        // and so on

    There’s a nice wrapper for this on cppreference.com that makes the second API cleaner:

    // helper type for the visitor #4
    template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
    // explicit deduction guide (not needed as of C++20)
    template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

    With this, you can just write:

      [&](std::string &&str) {  ... },
      [&](double d) {  ... },
      [&](bool b) {  ... },
      [&](JSONArray &&arr) {  ... },
      [&](JSONObject &&obj) {  ... }
      ), contents);

    Now you’re almost writing it as you would in a language that had first-class pattern matching on types.

    If you write it this way then you don’t need a separate JSONType and a load of the boilerplate code goes away. It’s less error-prone (there’s no way of expressing a thing that has two of the std::optional fields and no way for the field with a value to get out of sync with the enum telling you which one to look like. The space requirement is the maximum size of one of the values with a little bit of space for std::variant‘s discriminator, whereas in this version it’s the sum of the total. The version that uses std::variant is also easy to integrate with other templates or constexpr blocks.

    Skimming the rest of the article, there are lots of other strange things (including the fact that JSON is specified to be written in a unicode encoding and this is broken for any input that contains non-ASCII characters).

    TL;DR: Please don’t write C++ in the style advocated by this article.

    1. 1

      I do not disagree that std::variant is a more succinct and more type-safe alternative! But I don’t think your examples are simpler than what I have and simplicity was more of a guiding point. (None of this code was profiled, probably obvious).

      The std::variant API gets even more complex to deal with if you want to try to use multiple types. Like the equivalent of this in Standard ML:

      type X = X1 of string | X2 of string

      It’s a contrived example but there are valid reasons to want to model a sum type with multiple different fields of the same type.

      I’m not saying you can’t deal with this in std::variant. I’ve seen some examples of how you deal with it. But the code still seemed much more complex to me than what I think it should be. So I’m overall not a fan of std::variant at the moment, personally.

      Skimming the rest of the article, there are lots of other strange things (including the fact that JSON is specified to be written in a unicode encoding and this is broken for any input that contains non-ASCII characters).

      Totally fair! I could have called out that using ASCII was another simplification.

      1. 1

        Not to beat a dead horse, but it’s worth reflecting on why we’d ever even want an intermediate type for storing JSON values. After all, JSON’s design goals include being interoperable with languages which have relatively humble builtin types. The main issue with representation in C++ is that heterogeneous collections don’t fit easily into the type system.

        1. 1

          Do you mean something like storing all scalar types as strings and letting the user convert them as needed to their real type by calling JSONValue.toBool, toNumber, etc.?