Generic object creation from command line

2019-07-23 07:39发布

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);

}

1条回答
Juvenile、少年°
2楼-- · 2019-07-23 08:03

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_ptrs. 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

查看更多
登录 后发表回答