0

I have some JSON files in which I define objects of various types. The types are given as a field within the objects. I want to load the file and for each JSON object, create a new class of that type and pass the rest of the JSON data to its constructor.

The issue is that I'd rather not have a huge case statement matching the type and creating an object of that type. Here are some of the possibilities I've considered:

  1. Reflection. I don't know too much about it, but my understanding is that it might allow me to create a class in this manner. While I'm aware C++ doesn't provide this capability natively, I've seen a few libraries such as this one that might provide such functionality.

  2. Create an enum of class types. Create a template function that takes a type parameter from this enum and creates an object of that type. Use something like smart_enum to convert the string field.

Option 2 seems like a good one but I haven't been able to get this working. I've done extensive googling, but no luck. Does anyone know how I might go about doing this, or if there is a better option which I have not considered? Apologies if this has been answered elsewhere, perhaps under a term which I do not know; I have spent quite a lot of time trying to solve this problem and had no luck.

Please let me know if I can provide any additional information, and thank you.

Edit: here's an example of what I've tried to get option 2 working.

#include <iostream>
#include <string>

enum class Animals {
    Dog,
    Cat
};

class Dog {
public:
    std::string sound{"woof"};
};

class Cat {
public:
    std::string sound{"meow"};
};

template<Animals animal> void make_sound() {
    new animal();
    cout << animal.sound << endl;
}

int main() {
    make_sound<Animals::Dog>();
    make_sound<Animals::Cat>();

    std::exit(1);
}
Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
bin
  • 3
  • 3
  • There's no reflection available in c++, so you're left to option 2 only. – πάντα ῥεῖ Oct 26 '20 at 16:29
  • @πάνταῥεῖ Thank you, I've seen a few libraries that offer it, but that's what I thought. How would I implement option 2? I've been unable to get it working. I'm editing with an example of my non-working code in the main question. – bin Oct 26 '20 at 16:36
  • Here are some hints for starters: https://stackoverflow.com/questions/4007382/how-to-create-class-objects-dynamically It's a quite broad topic, _too broad_ to be answered here concisely. – πάντα ῥεῖ Oct 26 '20 at 16:39
  • Thanks, the factory idea looks like a potential solution. Is there a way to do what I was trying for in my code above, make an enum of potential classes and generate a template function for each, then convert strings to enum members? – bin Oct 26 '20 at 16:42
  • @πάνταῥεῖ Is there potentially a way to implement the factory idea in the question you linked with templates, enumerating the list of possible types and creating a factory for each? – bin Oct 27 '20 at 00:06
  • A template might help in some specific cases, but what you're basically looking for is _de-/serialization_. If you look that up, you'll find plethora of examples: https://www.google.com/search?rlz=1C1CHBF_deDE833DE833&sxsrf=ALeKk02q17n27r6MSiDT8-k9fUphIlgnpg%3A1603757396955&ei=VGWXX9rkOY_asAff0LnYBA&q=site%3Astackoverflow.com+%22c%2B%2B%22+serialize+objects+file&oq=site%3Astackoverflow.com+%22c%2B%2B%22+serialize+objects+file&gs_lcp=CgZwc3ktYWIQAzoECAAQR1D3rwFYnrkBYKLKAWgAcAJ4AIABqwGIAbkDkgEDMC4zmAEAoAEBqgEHZ3dzLXdpesgBCMABAQ&sclient=psy-ab&ved=0ahUKEwja3s3vvdPsAhUPLewKHV9oDksQ4dUDCA0&uact=5 – πάντα ῥεῖ Oct 27 '20 at 00:10
  • Respectively for JSON: https://www.google.com/search?rlz=1C1CHBF_deDE833DE833&sxsrf=ALeKk02jbzlxM0cWBK5lx9MfbKvnv-6Svg%3A1603757423843&ei=b2WXX7_3MpGbkwX1u4DgBQ&q=site%3Astackoverflow.com+%22c%2B%2B%22+serialize+objects+file+json&oq=site%3Astackoverflow.com+%22c%2B%2B%22+serialize+objects+file+json&gs_lcp=CgZwc3ktYWIQAzoECAAQR1CcxAJYw84CYMjYAmgAcAJ4AIABVogBowOSAQE1mAEAoAEBqgEHZ3dzLXdpesgBCMABAQ&sclient=psy-ab&ved=0ahUKEwj_6rb8vdPsAhWRzaQKHfUdAFwQ4dUDCA0&uact=5 – πάντα ῥεῖ Oct 27 '20 at 00:11

2 Answers2

1

There are a number of C++ JSON libraries that support mapping polymorphic or std::variant types based on some type selection strategy, which could rely on a type marker (e.g. "cat", "dog"), or alternatively the presence or absence of members. Lacking reflection, such libraries rely on traits. Typically the library provides built-in traits specializations for standard library types such as std::chrono::duration and std::vector, and supports custom specializations for user types. Some libraries offer convenience macros that can be used to generate the code for custom specializations.

The library ThorsSerializer has an example of encoding/decoding JSON for polymorphic types.

The library jsoncons has examples of encoding/decoding JSON for polymorphic types and the std::variant type

Daniel
  • 728
  • 7
  • 11
0

As noted in the comments, #1 is out, C++ lacks reflection (until P0194 gets adopted).

#2 still requires a big ol' switch block because you're still have to switch on a run-time type ID.

So, I'll propose #3: use a template to generate all those case statements you don't want to have to write (well, a map anyway).

This is the final code, which uses JSON for Modern C++ library for JSON parsing since that's the one that's available from godbolt :).

template <typename T, typename... Args>
struct option {
    using type = T;
    static_assert(std::is_constructible_v<T, Args...>, "constructor doesn't exist");

    static T create(const nlohmann::json& json) {
        return create_impl(json, std::index_sequence_for<Args...>{});
    }

    template <size_t... Is>
    static T create_impl(const nlohmann::json& json, std::index_sequence<Is...>) {
        return { json[Is].get<Args>()... };
    }
};

template <typename...>
struct to_string {
    using type = std::string_view;
};

template <typename... Options>
struct factory_builder {
    using variant = std::variant<typename Options::type...>;

    factory_builder(typename to_string<Options>::type... names) 
        : map { std::pair<std::string, std::function<variant(const nlohmann::json&)>> { names, [](const nlohmann::json& json) -> variant { return Options::create(json); } }... }
    { }

    variant operator ()(const nlohmann::json& json) {
        return map[json["type"].get<std::string>()](json["args"]);
    }
    
    std::map<std::string, std::function<variant(const nlohmann::json&)>> map;
};

Usage:

using factory_t = factory_builder<
    option<Dog, double, std::string>, // Dog & its constructor argument types
    option<Cat, int>                  // Cat & its constructor argument types
>;
factory_t factory("Dog", "Cat");      // String type identifiers for each option
auto my_object = factory( /* ... your JSON */ );

It assumes the JSON takes this form, where "type" is one of the string identifiers passed to the factory_builder constructor, and "args" is a list of arguments:

{ 
    "type": "TheTypeIdentifier",
    "args": [42, "whatever", false]
}

Demo: https://godbolt.org/z/3qfP9G


That's a lot of code, so let's break it down. First problem you need to solve is how to actually have a variable that can be more than one type, since C++ is strongly typed. C++17 provides std::variant for this, so we'll use that.

using result = std::variant<Dog, Cat>;
result example_result = Dog {};
example_result = Cat {};

Next, you need a way to generically describe, at compile time, how to construct an object: I used a simple struct with a template argument for the type, and a variable number of template arguments for the types going into that constructor:

template <typename T, typename... Args>
struct options;

Given a option<T, Args...>, how do you take a JSON array and pass those items to the constructor? With the nlohmann library, if you have a parsed JSON array called my_array and want to get index n and store it in an object of type T:

my_array[n].get<T>(); // accesses the array, converts, and returns a T&

To do that generically, I took the parameter pack of arguments and converted it into a parameter pack of increasing integers (0, 1, 2...) using std::index_sequence. Then I expanded the two parameter packs into T and n in the example above. It was convenient to put all this inside a static method of option<T, Args...>:

template <typename T, typename... Args>
struct option {
    /* ... */

    static T create(const nlohmann::json& json) {
        return create_impl(json, std::index_sequence_for<Args...>{});
    }

    template <size_t... Is>
    static T create_impl(const nlohmann::json& json, std::index_sequence<Is...>) {
        return { json[Is].get<Args>()... };
    }
};

That solves extracting arguments and calling a constructor for one type generically. Next problem is, how do you generate a function that switches on the type name and calls one of those Option<T, ...>::create functions?

For this solution, I used a map from strings to an std::function that takes JSON in and outputs our variant type:

template <typename... Options>
struct factory_builder {
    // note: using a typedef "type" in options<T, Args...> that just points to T
    using variant = std::variant<typename Options::type...>;

    factory_builder(/* one string for each type */) 
    {
        // TODO: populate map
    }

    variant operator ()(const nlohmann::json& json) {
        return map[json["type"].get<std::string>()](json["args"]);
    }
    
    std::map<std::string, std::function<variant(const nlohmann::json&)>> map;
};

Now we just need to build that map. First, a detour: how do you ask for one string per type in a parameter pack? I used a helper type that takes a template argument and has a typedef that is always a string. Expand into that, and you get a parameter pack of string types:

template <typename...>
struct to_string {
    using type = std::string_view;
};

Then, to populate the map, you can do that right from the initializer list:

using map_t = std::map<std::string, std::function<variant(const nlohmann::json&)>>;
factory_builder(...) : map {
    typename map_t::value_type {
        names, 
        [](const nlohmann::json& json) -> variant {
            return Options::create(json);
        }
    }... 
}

This is a little confusing, but it's expanding into something like this:

factory_builder(std::string dogName, std::string catName) : map {
    std::pair<...> { dogName, [](auto& j) { return Dog(...); } },
    std::pair<...> { catName, [](auto& j) { return Cat(...); } }
}

And that's it! Hope it helps.

parktomatomi
  • 3,851
  • 1
  • 14
  • 18
  • That's a great answer, thank you. Much appreciated, especially the great explanation! – bin Oct 27 '20 at 15:29
  • Quick follow-up question: how would you make this return an unique_ptr? – bin Oct 28 '20 at 23:04
  • The issue seems to be that I have to get the object like this: `std::get(obj).makesound()`. Is there any way I can get a clean pointer from this with the correct type attached, like `std::unique_ptr`? – bin Oct 29 '20 at 03:10
  • The proper way is to use methods of a variant is to call std::visit: `std::visit([](auto&& animal) { a.makesound(); }, obj);`. If you want to deal with pointers, you can use an abstract base type with virtual methods. Is that something you have the flexibility to do? – parktomatomi Oct 29 '20 at 06:32