1

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

}
Community
  • 1
  • 1
justHelloWorld
  • 6,478
  • 8
  • 58
  • 138
  • You can't just cram unrelated types into a return type. What would it look like at the call site? Either there needs to be a shared base class, or a union (or std::any, etc) for make to be useable – Caleth Jan 03 '17 at 11:47

1 Answers1

2

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

Quentin
  • 62,093
  • 7
  • 131
  • 191
  • @justHelloWorld What?? I've been bending myself over so it compiles in C++11 and you don't even check the compiler flags on Coliru ? ;) – Quentin Jan 03 '17 at 19:41
  • I've deeply read and tested your code, it's fantastic! There is only one problem: as you can see in my [linked question](http://stackoverflow.com/questions/41407732/command-line-arguments-for-different-constructors) at the beginning of this question, `make_abstract` gets `argv` and split it to the right type. As you can see in the linked question, this is possible through C++14 solutions (e.g. `std::tuple_element_t`). I wonder if it can be rewritten in C++11. – justHelloWorld Jan 04 '17 at 14:37
  • 1
    @justHelloWorld `std::tuple_element_t` is only a small helper that can be replaced with `typename tuple_element<...>::type`. `std::index_sequence` [can be fully implemented in C++11](http://stackoverflow.com/questions/17424477/implementation-c14-make-integer-sequence). `std::make_unique` is a one-liner function, that [can also be implemented in C++11](http://stackoverflow.com/questions/17902405/how-to-implement-make-unique-function-in-c11). You've got everything you need :) – Quentin Jan 04 '17 at 14:43