I have two C++ abstract classes Abs1
and Abs2
. Then I have:
`A : public Abs1`
`B : public Abs1`
`C : public Abs2`
`D : public Abs2`
Now, I'm trying to create objects from command line arguments and I have to do rewrite the public factory function make_abstract
in the linked question, something like:
std::unique_ptr<Abs1> makeAbs1 (int argc, const char*argv[])
{
if (argc == 1) {
return nullptr;
}
const std::string name = argv[1];
if (name == "A") {
return detail::make_abstract<A, std::tuple<int, std::string, int>>(argc, argv);
} else if (name == "B") {
return detail::make_abstract<B, std::tuple<int, int>>(argc, argv);
}
}
std::unique_ptr<Abs2> makeAbs2 (int argc, const char*argv[])
{
if (argc == 1) {
return nullptr;
}
const std::string name = argv[1];
if (name == "C") {
return detail::make_abstract<C, std::tuple<int>>(argc, argv);
} else if (name == "D") {
return detail::make_abstract<D, std::tuple<int, float>>(argc, argv);
}
}
As you can see this is terribly redundant. How can I do a generic version of this? In this version we can pass as many implemented class as we want, so the if
cascade is not a solution. Notice that we cannot modify any of these classes.
I was thinking that maybe variadic templates could help, but I can't figure out many problems:
template <typename T, typename ...Ts>
std::unique_ptr<T> make (int argc, const char*argv[]){
const std::string name = argv[1];
for(Ti : Ts) //this is obviously wrong
if(typeid(Ti).name == name)
return detail::make_abstract<T, std::tuple</*Here shoudl be different for every Ti*/>>(argc, argv);
}
Ooooh, this was fun :)
[TL;DR: there's a live example at the bottom]
I have implemented two layers of mapping on top of your detail::make_abstract
function(s). Let's start from the calling code:
int main(int argc, char **argv) {
std::unique_ptr<Abs1> p1;
std::unique_ptr<Abs2> p2;
makeEverything(argc, argv, p1, p2);
}
Here we are calling makeEverything
with argc
, argv
, and a list of std::unique_ptr
s. After the function ends, one of the pointers will hold an object of the correct type.
Let's go deeper.
inline void makeEverything(int, char**) { }
template <class Abs, class... Abses>
void makeEverything(int argc, char **argv,
std::unique_ptr<Abs> &abs, std::unique_ptr<Abses> &... abses) {
abs = makeAbs<Abs>(argc, argv);
if(!abs)
makeEverything(argc, argv, abses...);
}
This is your usual recursive variadic function template: take the first pointer, try to construct an object for it. If it failed, throw it away and retry with the next one. You could put some error handling inside the base-case overload at the top: it will be called when no object could be constructed at all.
So now we know which one of Abs1
, Abs2
or whatever is the desired base class.
Let's go deeper.
template <class Abs>
using Factory = std::unique_ptr<Abs>(int, char **);
template <class Abs>
using FactoryMap = std::map<std::string, Factory<Abs>*>;
template <class Abs>
struct Factories {
static const FactoryMap<Abs> map;
};
template <class Abs>
std::unique_ptr<Abs> makeAbs(int argc, char **argv) {
if (argc < 2)
return nullptr;
return Factories<Abs>::map.at(argv[1])(argc, argv);
}
makeAbs
checks and retrieves argv[1]
. Then it uses it as a key into a map of factory functions, to retrieve the factory corresponding to that name, and then call it and return the resulting object.
If no object of that name is known, std::map::at()
will throw std::out_of_bounds
. Of course, you can change that error handling
Now let's see how we can populate the factory maps, it is actually quite easy:
template <>
FactoryMap<Abs1> const Factories<Abs1>::map {
{"A", detail::make_abstract_erased<Abs1, A, std::tuple<int, std::string, int>>},
{"B", detail::make_abstract_erased<Abs1, B, std::tuple<int, int>>}
};
You just have to provide a definition of FactoryMap<Abs>::map
for each Abs
you wish to use. Since this is an object definition, this should be put inside a .cpp file. Note that, as a bonus, you can add new classes and their mappings without recompiling anything else!
Final piece of the puzzle: detail::make_abstract_erased
. You haven't provided the declaration of detail::make_abstract
, but it looks like it returns std::unique_ptr<T>
, with T
being its first template argument.
Given that C++ does not allow converting between function pointers that differ in return types (and for good reasons), we need that additional layer just to wrap detail::make_abstract
and perform the conversion:
namespace detail {
template <class Abs, class T, class Params>
std::unique_ptr<Abs> make_abstract_erased(int argc, char **argv) {
return make_abstract<T, Params>(argc, argv);
}
}
And that's it!
See it live on Coliru