Search code examples
c++11templatesvariadic-templates

Firing virtual function instead of member template function with variadic arguments, for specific types


I have a template member function which I want to call for most of the types, but for some specific types, I need virtual member functions to be called instead.

Something like this (please don't focus on the "Factory design", its just for bringing up the c++ issue, not because this is a design where Factory necessarily needed):

// factory 
class Factory
{
    // default create function
    template<typename T, typename... Args>
    T* create(Args&&...parametes) const
    {
        return new T(std::forward<Args>(parametes)...);
    }

    // Handling creation for type A
    virtual A* create(int i_i, const std::string& i_s) const =0;

    // Handling creation for type C
    virtual C* create(int i_i) const = 0;
};

Use for example:

Factory* pFactory = new DerivedFactoryObject;
A* pA = pFactory->creat<A>(4,"Test");

But it always calls the template function and not the virtual one for A or C.

I tried to use template specialization like this:

    template<>
    template<typename... Args>
    A* create<A>(Args&&...parametes) const
    {
        return createA(std::forward<Args>(parametes)...);
    }

     virtual A* createA(int i_i, const std::string& i_s) const =0;

But actually it is impossible to do a partial specialization for functions, and indeed, the compilation failed.

So how can I do it?


Solution

  • It took me several intensive days to figure out how to do it, so I would be happy to give here my conclusion as a tutorial for helping other to understand the details that we can fall with. It's opens for corrections and improvements, that's why I write it here as a question.

    In order to show the issues involved, I explain the subject step by step with a tiny example. In this example, we wish to build an objects creator using factory, where we can use different type of factories to get different ways of object creation.

    Basic version:

    First, lets begin with a basic version of our factory.

    // forward decleration
    class Creator;
    
    // factory 
    class Factory
    {
    public:
        Factory() = default;
        virtual ~Factory() = default;
    private:
        // create is private to prevent using it, only class Creator can use it.
        template<typename T, typename... Args>
        T* create(Args&&...parametes) const
        {
            return new T(std::forward<Args>(parametes)...);
        }
    
        // allow only calss Creator to use factoy's create function
        friend class Creator;
    };
    
    // for having cleaner code
    using FactorySPtr = std::shared_ptr<Factory>;
    

    This factory can create different types of objects using its template function create which is private and only a Creator object can use it - that’s why we declare class Creator as a friend.

    The Creator is simple as well:

    class Creator
    {
    public:
        Creator(FactorySPtr  i_spFactory) : m_spFactory(i_spFactory) {}
    
        template<typename T, typename... Args>
        std::shared_ptr<T> create(Args&&...parametes) const
        {
            T* p = m_spFactory->create<T>(std::forward<Args>(parametes)...);
            return std::shared_ptr<T>(p);
        }   
    private:
        FactorySPtr  m_spFactory;
    };
    

    When constructed, it stores the factory it will use to create objects.

    For showing how it is used, lets assume we have the follow objects A ,B and C:

    struct A
    {
        A(int i_i, const std::string& i_s) : i(i_i), s(i_s) {}
        int i = 0;
        std::string s;
    };
    
    struct B
    {
        B(const std::vector<float>& i_v) : v(i_v) {}
        std::vector<float> v;
    };
    
    struct C
    {
        C(int i_i) : i(i_i) {}
        int i = 0;
    };
    

    Then we can use the creator as follows:

    Creator creator(std::make_shared<Factory>());
    
    std::shared_ptr<A> spA = creator.create<A>(4, "test");
    std::shared_ptr<B> spB = creator.create<B>(std::vector<float>({0.1f,0.2f,0.3f}));
    std::shared_ptr<C> spC = creator.create<C>(67);
    

    Firing virtual member functions instead of a template member function:

    Now lets say that we need the factory to allow creating object A and C in different ways.

    For that, we are adding the Factory two virtual functions that will fired instead of the template create function for when the handled objects are A or C. We then can create derived factory classes that can treat those objects in different ways as we wish.

    For simplicity, lets call these virtual functions in - replacement functions, because they intend to be fired instead of the template one for the specific types.

    !! Take care a replacement function is the same form as the template one.

    !! Take care a replacment const/non const type is the same as the template one. In our example the template create function is a const function so we have to make the replacement functions const as well.

    // factory 
    class Factory
    {
    public:
        Factory() = default;
        virtual ~Factory() = default;
    
    private:
        // create is private to prevent using it, only class Creator can use it.
        template<typename T, typename... Args>
        T* create(Args&&...parametes) const
        {
            return new T(std::forward<Args>(parametes)...);
        }
    
        // function replacement for A
        virtual A* create(int i_i, const std::string& i_s) const
        {
            return new A(i_i,i_s);
        }
        
        // function replacement for C
        virtual C* create(int i_i) const
        {
            return new C(i_i);
        }
    
        // allow only calss Creator to use factoy's create function
        friend class Creator;
    };
    using FactorySPtr = std::shared_ptr<Factory>;
    

    Lets create a derived Factory that does a different treatment when creating A and C

    // eve factory 
    class FactoryEve : public Factory
    {
    public:
        FactoryEve() = default;
        virtual ~FactoryEve() = default;
    
    private:
        virtual A* create(int i_i, const std::string& i_s) const
        {
            A* p = new A(i_i, i_s);
            p->s += "_Hey!";
            return p;
        }
    
        virtual C* create(int i_i) const
        {
            C* p = new C(i_i);
            p->i += 5;
            return p;
        }
    };
    

    But this won’t work!

    Lets check it.

    In the follow run, we construct a creator with a the FactoryEve type in order to get different treatment when creating A and C.

    Tracing the calls shows that all creations are done using the template Factory::create function, but not the replacement functions.

    Creator creator(std::make_shared<FactoryEve>());
    
    std::shared_ptr<A> spA = creator.create<A>(4, "test");                              // Bug -- created by template<typename T> T* Factory::create
    std::shared_ptr<B> spB = creator.create<B>(std::vector<float>({0.1f,0.2f,0.3f}));   // Ok  -- created by template<typename T> T* Factory::create
    std::shared_ptr<C> spC = creator.create<C>(67);                                     // Bug -- created by template<typename T> T* Factory::create
    

    Why is it?

    Because we actually call the template form only - see the function Creator::create which contains the calling line:

    T* p = m_spFactory->create<T>(std::forward<Args>(parametes)...);
    

    We call create with a template parameter which means - calling the template function only and the compiler does exactly what we asked it to do.

    Thus, to allow the compiler to find a match of also non-template functions, we have to change the line to.

    T* p = m_spFactory->create(std::forward<Args>(parametes)...);
    

    In that way , the compiler takes the best match for type T. If there is an explicit function for that type, it will prefer it, otherwise, it will use the template one.

    !! When calling a template function that may have non-template replacements functions, or different templates parameters functions, call it without a template parameter.

    But now, if you remove the template parameter, you will get a compiler error :(

    Why is it?

    Because the compiler can not find the correct function based on return type which is the form of our functions.

    !! Replacement can not be done based on a return type.

    When we used the template parameter the compiler knew exactly what function to take, but we need to call create without a template parameter to allow replacement by non-template functions.

    Thus, we must change our functions form to have the object type IN the function arguments .

    Here is the fixed code for Factory object, and the derived class FactoryEve should be fixed accordingly.

    // factory 
    class Factory
    {
    public:
        Factory() = default;
        virtual ~Factory() = default;
    
    private:
        // create is private to prevent using it, only class Creator can use it.
        // return object pointer is placed as a parameter to allow the compiler to find the correct function
        
        template<typename T, typename... Args>
        void create(T*& o_p, Args&&...parametes) const
        {
            o_p = new T(std::forward<Args>(parametes)...);
        }
    
        // function replacement for A
        virtual void create(A*& o_p, int i_i, const std::string& i_s) const
        {
            o_p = new A(i_i, i_s);
        }
    
        // function replacement for C
        virtual void  create(C*& o_p, int i_i) const
        {
            o_p = new C(i_i);
        }
    
        // allow only calss Creator to use factoy's create function
        friend class Creator;
    };
    

    The object Creator should be fixed as well.

    class Creator
    {
    public:
        Creator(FactorySPtr  i_spFactory) : m_spFactory(i_spFactory) {}
    
        template<typename T, typename... Args>
        std::shared_ptr<T> create(Args&&...parametes) const
        {
            T* p;
            m_spFactory->create(p, std::forward<Args>(parametes)...);
            return std::shared_ptr<T>(p);
        }   
    private:
        FactorySPtr  m_spFactory;
    };
    

    Ok, if we run it now, we will get an improvement, but yet, it won’t do everything as needed.

    Tracing the calls shows that for object C, it uses the FactoryEve::create for C as expected, but for object A it still uses the template function of the Factory base class .

    Creator creator(std::make_shared<FactoryEve>());
    
    std::shared_ptr<A> spA = creator.create<A>(4, "test");                              // Bug -- created by template<typename T> Factory::create 
    std::shared_ptr<B> spB = creator.create<B>(std::vector<float>({0.1f,0.2f,0.3f}));   // Ok  -- created by template<typename T> Factory::create
    std::shared_ptr<C> spC = creator.create<C>(67);                                     // Ok  -- created by FactoryEve::create(C*& o_p, int i_i)
    

    Why is it?

    Because the parameters pack types for input parameters (4,”test”) which we gives for creating A is treated as:

    int, const char[5]

    Thus, in case of object A, for the line m_spFactory->create(p, std::forward(parametes)...); the compiler searches for a function with the form of

    void Factory::create(A*&, int&&, const char[5]&) const

    But the virtual function we declare for A is with different form and has std::string instead of char[5]

    void create(A*& o_p, int i_i, const std::string& i_s) const

    That’s why it doesn’t work with A, but only for C.

    The problem is that when compiler chose the match function, it doesn’t consider conversions.

    So what can we do? We do not want to force the application to use std::string but leave it friendly, we don’t want to write virtual function for every possible converted type because it makes our code boiled with many functions, and for sure we will forget some type … it seems really frightening !

    Luckily, we have a solution. Lets use the parameters packs itself as input argument for our replacement functions. In this way, it ensures that whatever types the compiler got from the application call, our functions will have the same form!

    !! When replaciong a function with parameters pack, prefer to use function with parameter pack too.

    In principle, we would like to do something like this

        // function replacement for A
        template<typename... Args>
        virtual void create(A*& o_p, Args&&...parametes) const
        {
            o_p = new A(i_i, i_s);
        }
    

    But we can’t, because virtual functions can not be template !

    Haa! it seems that we just go from a problem to a problem.

    Well, fortunately we can solve it easily by using template replacement functions that calls the virtual functions, like this:

    (Note that virtual functions now has each a unique name createA and create C)

    // factory 
    class Factory
    {
    public:
        Factory() = default;
        virtual ~Factory() = default;
    
    private:
        // create is private to prevent using it, only class Creator can use it.
        // return object pointer is placed as a parameter to allow the compiler to find the correct function
        
        template<typename T, typename... Args>
        void create(T*& o_p, Args&&...parametes) const
        {
            o_p = new T(std::forward<Args>(parametes)...);
        }
    
        // function replacement for A
        template<typename... Args>
        void create(A*& o_p, Args&&...parametes) const
        {
            // calling virtual function which create A 
            createA(o_p, std::forward<Args>(parametes)...);
        }
    
        // function replacement for C
        template<typename... Args>
        void create(C*& o_p, Args&&...parametes) const
        {
            // calling virtual function which create C 
            createC(o_p, std::forward<Args>(parametes)...);
        }
    
        virtual void createA(A*& o_p, int i_i, const std::string& i_s) const
        {
            o_p = new A(i_i, i_s);
        }
    
        virtual void  createC(C*& o_p, int i_i) const
        {
            o_p = new C(i_i);
        }
    
        // allow only calss Creator to use factoy's create function
        friend class Creator;
    };
    

    In this way, we replace the a general create template function while using the same parameters pack which makes sure we get the same form, and still, we have the virtual mechanism to allow different factory types for different methods of creation.

    And now it works!

    Don’t forget to modify the functions names in FactoryEve to createA and createC as well, and now if you run, you will get exactly what you wish for:

    Creator creator(std::make_shared<FactoryEve>());
    
    std::shared_ptr<A> spA = creator.create<A>(4, "test");                              // Ok  -- created by FactoryEve::createA
    std::shared_ptr<B> spB = creator.create<B>(std::vector<float>({0.1f,0.2f,0.3f}));   // Ok  -- created by template<typename T> Factory::create
    std::shared_ptr<C> spC = creator.create<C>(67);                                     // Ok  -- created by FactoryEve::createC
    

    Final words:

    For using virtual functions to replace a template member function with parameters pack, we actually need not to use them as replacements but to call them from a different template replacements functions.

    • Take care the replacement function will have the same form as the template one - take care also for the const/non const function type.
    • When calling a template function that may have non-template replacements functions, or different templates parameters functions, call it without a template parameter.
    • Replacement can not be done based on a return type. Thus, if your type is the only one to identify the function, place it as a parameter.
    • When replacing a function with parameters pack, prefer to use a replacement function with the same parameter pack.