Warm tip: This article is reproduced from stackoverflow.com, please click
c++ inheritance templates variadic-templates factory

In C++, how can I create a heterogenous vector containing variafic templated objects?

发布于 2020-04-08 09:42:45

Hi StackOverflow Community !

I was playing at work with variadic templates, inheritance and an abstract factory pattern and am now struggling to make it work together. It seems I have reach the farthest of what I currently know about these subjects, so if you can point me a hint or a code example, you would get my entire gratitude ! Thanks in advance ;)

Here is the context (my apologies ! there is a few lines of code...):

I have a Base class

template<typename... Params>
class P
{
    public:
        virtual void compute(Params&... ps) = 0;
        // other things ...

};

and Derived classes

template<typename... Params>
class AP : public P<Params...>
{
    public:
        void compute(Params&... ps) override { _compute(std::forward<Params&>(ps)...); }
    private:
        void _compute(std::string& str) {std::cout << "AP::compute str " << str << std::endl;}
};
using A = AP<std::string>;

template<typename... Params>
class BP : public P<Params...>
{
    public:
        void compute(Params&... ps) override { _compute(std::forward<Params&>(ps)...); }
    private:
        void _compute(int& i) {std::cout << "BP::compute i " << i << std::endl;}
};
using B = BP<int>;

Up to here, no problems ! If I make a small main(), this works without any problem :

int main()
{
    std::unique_ptr<P<int>> p1 = std::make_unique<B>();
    int i = 15;
    p1->compute(i);

    std::unique_ptr<P<std::string>> p2 = std::make_unique<A>();
    std::string str = "abc";
    p2->compute(str);
}

However, we can add some more : the Base classes for the factory. (These will be used with other classes than my class P... if you were wondering why :) )

template<typename Base>
class Creator
{
    public:
        virtual std::unique_ptr<Base> create() = 0;
};

template<class Key, class T>
class Factory
{
    public:
        void store(Key key, std::unique_ptr<Creator<T>>&& creator)
        {
            _crs[key] = std::move(creator);
        }

        std::unique_ptr<T> create(Key key)
        {
            return _crs[key]->create();
        }

    private:
        std::map<Key, std::unique_ptr<Creator<T>>> _crs;
};

and their implementations to be able to build P-related objects :

template<typename Derived, typename... Params>
class PCreator : public Creator<P<Params...>>
{
    public:
        std::unique_ptr<P<Params...>> create() override
        {
            return std::make_unique<Derived>();
        }
};

template<typename... Params>
class PFactory : public Factory<std::string, P<Params...>>
{
    public:
        PFactory()
        {
            this->store("int", std::make_unique<PCreator<BP<int>>>);
            this->store("string", std::make_unique<PCreator<AP<std::string>>>);
        }
        // create() and store() methods inherited
};

If I instantiate PFactory, compiler can obviously not do its job, because it wants template arguments for PFactory which would forward them to Factory<std::string, P<Params...>>.

But than, my factory would only be able to create one single "type" of P object, the one that can use these Params. This is how far I went alone (and sadly no one of my colleagues has the abilities to help me...)

My goal is to be able to write something like this :

class Thing
{
    const std::array<std::string, 2> a = {"one", "two"};
    public:
        Thing()
        {
            PFactory f;
            for(const auto& e : a)
                _ps[e] = std::move(f.create(e));
        }

        void compute()
        {
            int i = 100;
            std::string str = "qwerty";
            // additional computations...
            _ps["one"]->compute(i);
            // additional computations...
            _ps["two"]->compute(str);
        }

    private:
        std::map<std::string, std::unique_ptr<P>> _ps;
};

Here is the PoC I tried to work and rework on CompilerExplorer and from where comes the sources above.

Any help will be hugely appreciate !


[Edit] Yes, I was delusional to think I could trick the compiler to create various method signatures with runtime infos.

Solutions' sum-up :

(@walnut: thanks!) let compute take std::any or something like that

I do not know std::any very well, but after rtfm-ing CppReference, it could do the job, accepting the fact, that I need to cast back the parameter to what I need it to be in my Derived classes (and deal with the exception). Sadly, on the real project, compute() can take more than one parameter (the reason I played with variadic templates... I did not want to care about the number or the types of the parameters in each compute method in each Derived class), so it would force me to create compute(const std::any&) and compute(const std::any&, const std::any&), etc.

(@MaxLanghof: thanks!) One (ugly) solution is to provide all possible compute overloads as virtual functions by hand.

Yes, your right, I find it odd too (I would not go as far as «ugly», but I have no prettier solution yet, so...), but it is working. The drawback in here is that I won't be able to store the class P (and related classes) in its own library as I wanted at the beginning to separate concerns («MainProgram» playing with Ps deriving from lib::P).

(@MaxLanghof: thanks!) to do the entire _ps mapping at compile-time.

I have not enough experience and knowledge in C++ yet to achieve such a thing. I need to work on that and if someone has specific links (I mean : not the first link on Google ;) ) or examples, I would be glad to learn.

Thanks for your answer so far !


[Edit] Hi ! Sorry for the delay, I just got back on this project and thank you a lot for your ideas and experience ! It means a lot !

I worked a bit with @Caleth 's and with @KonstantinStupnik 's work (really thank you for your examples : they helped me a lot to understand what I was doing !) and arrived to this point with my test case : https://gcc.godbolt.org/z/AJ8Lsm where I hit a std::bad_any_cast exception, but don't understand why...

I suspect an issue with the pass-by-reference or the way I use a lambda to save the compute method in the std::any, but cannot be sure. I tried to expand the types received in the AnyCallable<void>::operator() to find a difference with the stored function in P's constructor, but seems the same to me.

I tried to pass &AP::compute to P's constructor, but then the compiler cannot deduce the parameter types anymore...

Thank you all for your time, your help and your advice !

Questioner
Psyko
Viewed
22
Caleth 2020-02-03 18:55

You can type-erase the parameters, so long as you can specify them at the call site.

Minimally:

#include <functional>
#include <any>
#include <map>
#include <iostream>

template<typename Ret>
struct AnyCallable
{
    AnyCallable() {}
    template<typename F>
    AnyCallable(F&& fun) : AnyCallable(std::function(fun)) {}
    template<typename ... Args>
    AnyCallable(std::function<Ret(Args...)> fun) : m_any(fun) {}
    template<typename ... Args>
    Ret operator()(Args&& ... args) 
    { 
        return std::invoke(std::any_cast<std::function<Ret(Args...)>>(m_any), std::forward<Args>(args)...); 
    }
    template<typename ... Args>
    Ret compute(Args ... args) 
    { 
        return operator()(std::forward<Args>(args)...); 
    }
    std::any m_any;
};

template<>
struct AnyCallable<void>
{
    AnyCallable() {}
    template<typename F>
    AnyCallable(F&& fun) : AnyCallable(std::function(fun)) {}
    template<typename ... Args>
    AnyCallable(std::function<void(Args...)> fun) : m_any(fun) {}
    template<typename ... Args>
    void operator()(Args&& ... args) 
    { 
        std::invoke(std::any_cast<std::function<void(Args...)>>(m_any), std::forward<Args>(args)...); 
    }
    template<typename ... Args>
    void compute(Args ... args) 
    { 
        operator()(std::forward<Args>(args)...); 
    }
    std::any m_any;
};

using P = AnyCallable<void>;

void A(std::string& str) {std::cout << "AP::compute i " << str << std::endl;}
void B(int i) {std::cout << "BP::compute i " << i << std::endl;}

class Thing
{
    public:
        Thing(){}

        void compute()
        {
            int i = 100;
            std::string str = "qwerty";
            // additional computations...
            ps["one"].compute<int>(i);
            // additional computations...
            ps["two"].compute<std::string&>(str);
        }

    private:
        std::map<std::string, P> ps = { { "one", B }, { "two", A } };
};

Or with all the Factory