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.