Search code examples
jsonc++17variadic-templatesinitializer-list

How to use std::initializer_list constructor with different types to handle nested braced initializer lists


I am looking at the nlohmann json library and I see that the author made it possible to construct json objects like so:

json j2 = {
  {"pi", 3.141},
  {"happy", true},
  {"name", "Niels"},
  {"nothing", nullptr},
  {"answer", {
    {"everything", 42}
  }},
  {"list", {1, 0, 2}},
  {"object", {
    {"currency", "USD"},
    {"value", 42.99}
  }}
};

and below the example he states the following:

Note that in all these cases, you never need to "tell" the compiler which JSON value type you want to use. If you want to be explicit or express some edge cases, the functions json::array() and json::object() will help:

I was intrigued by this and tried implementing my own simplified version of this behavior, but I was unsuccessful. I am having trouble getting the initializer list to accept different types at the same time. I also tried analyzing the actual source code of the nlohmann library, and I see that his json object also has one constructor that accepts an std::initializer_list that holds some (as far as I understand) fixed type, but I do not understand how that allows std::initializer_list to understand the nested braced initializer lists like in the example.

The condition to determine if the initializer list is representing a JSONArray or JSONMap should be as follows:

If every nested element in the list is itself an array of length 2 where the first element is of type can be used to construct a JSONString (I am thinking about using something like std::is_constructible_v<JSONString, T>), and the second element is something that can be used to construct a JSONObject, then we may deduce that the whole initializer list represents a JSONMap, otherwise we treat it as a JSONAarray

In the end I want to end up with code that looks something like this:

#include <iostream>
#include <vector>
#include <map>
#include <variant>

class JSONObject;

using JSONString = std::string;
using JSONNumber = double;
using JSONBool = bool;
using JSONNull = nullptr_t;
using JSONArray = std::vector<JSONObject>;
using JSONMap = std::map<std::string, JSONObject>;

class JSONObject {
    public:
        JSONObject() : var{JSONMap{}} {}

        template <typename T>
        JSONObject(std::initializer_list<T> list) {
            // I do not understand how to implement this
        }

    private:
        std::variant<JSONString, JSONNumber, JSONBool, JSONNull, JSONArray, JSONMap> var;
};

int main() {
    JSONObject jsonObj = {
        {"pi", 3.141},
        {"happy", true},
        {"name", "Niels"},
        {"nothing", nullptr},
        {"answer", {
            {"everything", 42}
        }},
        {"list", {1, 0, 2}},
        {"object", {
            {"currency", "USD"},
            {"value", 42.99}
        }}
    };

    return 0;
}

While doing some research, I also came accross an idea to make a variadic template constructor for JSONObject like so:

template <typename... Args> 
JSONObject(Args&&... args) {
   // some fold expression to deduce how to construct the variant
}

but even with this, I am having trouble dealing with the nesting braced initializer lists


Solution

  • A std::initializer_list is homogeneous with respect to type -- all the elements are of the same type. The trick is creating a single type that can hold different values. This is where std::variant come into play.

    The tricky part about using std::variant is that it is not recursive, i.e. it cannot hold a value of its own type. There are variant implementations that are recursive such as rva::variant and Boost.

    Also, if you want to understand the details, here is a good tutorial on how to implement a recursive variant type.

    Update

    The following code is a rough sketch of using the rva::variant for a json like type. The prototype allows for a natural initialization syntax much like nlohmann json. It uses the same technique to differentiate between an obect and array.

    Sample Code

    #include <iostream>
    #include <map>
    #include <string>
    #include <vector>
    #include "variant.hpp"
    
    using std::cin, std::cout, std::endl;
    
    using JsonBase = rva::variant<
        std::nullptr_t,
        std::string,
        double,
        bool,
        std::map<std::string, rva::self_t>,
        std::vector<rva::self_t>
        >;
    
    class JsonValue : public JsonBase {
    public:
        using JsonBase::JsonBase;
        using InitializerList = std::initializer_list<JsonValue>;
    
        JsonValue(InitializerList init) {
            bool is_object = std::all_of(init.begin(), init.end(), [](const auto& value) {
                if (std::holds_alternative<std::vector<JsonBase>>(value)) {
                    const auto& arr = std::get<std::vector<JsonBase>>(value);
                    return arr.size() == 2 and std::holds_alternative<std::string>(arr[0]);
                }
                return false;
            });
            if (is_object) {
                std::map<std::string, JsonBase> m;
                for (const auto& value : init) {
                    const auto& arr = std::get<std::vector<JsonBase>>(value);
                    const auto& key = std::get<std::string>(arr[0]);
                    m.emplace(key, arr[1]);
                }
                *this = m;
            } else {
                std::vector<JsonBase> vec;
                for (auto&& value : init)
                    vec.emplace_back(value);
                *this = vec;
            }
        }
    };
    
    std::ostream& operator<<(std::ostream& os, const JsonBase& value) {
        if (std::holds_alternative<std::nullptr_t>(value))
            os << "{ }";
        else if (std::holds_alternative<std::string>(value))
            os << "\"" << std::get<std::string>(value) << "\"";
        else if (std::holds_alternative<double>(value))
            os << std::get<double>(value);
        else if (std::holds_alternative<bool>(value))
            os << std::boolalpha << std::get<bool>(value);
        else if (std::holds_alternative<std::map<std::string, JsonBase>>(value)) {
            os << "{ ";
            for (const auto& [key, value] : std::get<std::map<std::string, JsonBase>>(value))
                os << "{ " << key << " : " << value << " } ";
            os << "]";
        }
        else if (std::holds_alternative<std::vector<JsonBase>>(value)) {
            os << "[ ";
            for (const auto& elem : std::get<std::vector<JsonBase>>(value))
                os << elem << " ";
            os << "]";
        }
        return os;
    }
    
    int main(int argc, const char *argv[]) {
        JsonValue str = "abc";
        cout << str << endl;
    
        JsonValue dbl = 1.0;
        cout << dbl << endl;
    
        JsonValue bol = true;
        cout << bol << endl;
    
        JsonValue vec = {
            "abc", "def", 1.0, true
        };
        cout << vec << endl;
    
        JsonValue m = {
            { "key0", 2.0 },
            { "key1", true }
        };
        cout << m << endl;
    }
    

    Output

    "abc"
    1
    true
    [ "abc" "def" 1 true ]
    { { key0 : 2 } { key1 : true } ]