0

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 !

mscherer
  • 97
  • 1
  • 3
  • 12
  • 1
    I am not sure what the point of all of this is, but fundamentally: `_ps["one"]` and `_ps["two"]` have the same static type. So either `compute` must be a template that accepts different types, in which case it cannot be `virtual` or it must be overloaded for the different types that it accepts in the base class. Currently `compute` is a single non-overloaded function in your base class. That cannot work. – walnut Jan 31 '20 at 12:22
  • The only way to make it work this way that I see is to let `compute` take `std::any` or something like that, but then still the caller must know the derived type of the objects in `_ps` apriori in order to not provide the wrong argument type, in which case using a container with a base class pointer is pointless. – walnut Jan 31 '20 at 12:23
  • The compiler needs to know the type of `(*_ps["..."])` at compile-time. Since that type is decided at compile-time, it cannot depend on a run-time value. You want that type to have `compute(i)` and `compute(str)` methods. But you cannot have member functions that are `virtual` and templated. One (ugly) solution is to provide all possible `compute` overloads as virtual functions by hand. What behavior do you expect for `_ps["one"]->compute(str);`? – Max Langhof Jan 31 '20 at 12:24
  • Another possible solution is to do the entire `_ps` mapping at compile-time. But then you cannot (easily) use run-time string values instead of e.g. `"one"`. In general, type erasure requires that either a) the entire interface is part of the (non-templated) base class, or b) the caller/user code knows what type they need to retrieve. You currently fulfill neither, and you'll have to choose one of these options if you want a heterogeneous container. – Max Langhof Jan 31 '20 at 12:25
  • `template class AP` isn't really working, only `AP` would work. – Jarod42 Jan 31 '20 at 13:58
  • @MaxLanghof ["b) the caller/user code knows what type they need to retrieve"] how could I reflect your sentence in my sources ? I am not sure to understand that well (apologies! and thanks for your answers!) – mscherer Jan 31 '20 at 16:18
  • @Psyko Consider [`std::any`](https://en.cppreference.com/w/cpp/utility/any). It performs universal type erasure. You can store anything in it, but you consequently have no action that you could perform on the type-erased value (other than destruction I guess). But that doesn't mean it's useless, because you can retrieve the stored object as long as you (the user/caller) know what type is in there _at compile time_, by using [`std::any_cast`](https://en.cppreference.com/w/cpp/utility/any/any_cast). You could use this to store objects of different types in a heterogeneous container. But unless.. – Max Langhof Feb 03 '20 at 08:40
  • ...a user of that container knows which elements would have which types (at compile time), you cannot do anything meaningful with the elements. [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant) is another example. To properly use type erasure in your code, the `P` would have to be a non-templated base class and the derived classes would have to be the templated. Then you could have e.g. a `template computeWith(P&, Ts&&...ts)` that casts from the base (`P`) to the templated version and then calls its `compute` accordingly. That'll work but might be inconvenient. – Max Langhof Feb 03 '20 at 10:06
  • For an example with `std::any`, have a look at [my answer here](https://stackoverflow.com/a/45718187/2610810) – Caleth Feb 03 '20 at 10:20
  • quick and dirty prototype: https://psty.io/p?q=18f8a – Konstantin Stupnik Feb 03 '20 at 11:10
  • You need to exactly match the argument types. `bad_any_cast` means you didn't. [See fixed](https://gcc.godbolt.org/z/v3-atp) You need `const std::string &` in main, and `int &, const std::string &` in AP::compute – Caleth Feb 19 '20 at 16:19

1 Answers1

0

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

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • Thanks a lot @Caleth ! Your example made me understand a lot of things so far. I used your work to try to better mine which lead to the second edit of my post (sadly). Thanks again for your help ! – mscherer Feb 19 '20 at 16:05