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 project example. In this project, we wish to build an objects creator using factory, where we can use different type of factories to get different ways to do the process of an 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);
Overriding member template 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 override 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.
!! When overriding, take care the override function will have the same form as the template one.
!! When overriding, take care for the const/non const function type. In our example the template create function is const function so we have to make the override 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)...);
}
// override function for A
virtual A* create(int i_i, const std::string& i_s) const
{
return new A(i_i,i_s);
}
// override function 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 overriding 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 overrides, or different templates parameters, 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.
!! Overriding 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 overriding 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)...);
}
// override function 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);
}
// override function 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 works with A, but only for C.
The problem is that when compiler does overriding , 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 overriding functions. In this way, it is sure that whatever types the compiler got from the application call, our functions will have the same form!
!! When overriding a function with parameters packs, prefer to override with parameter packs too.
In principle, we would like to do something like this
// override function 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 overriding using template 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)...);
}
// override function 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)...);
}
// override function 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);
}
// override function for C
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 override the a general create template function while using the same parameters pack which makes sure we get the same form, but 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 override a template member function with parameters pack, we actually need to not use them for overriding but to call them from an overriding template functions.
- When overriding, take care the override 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 overrides, or different templates parameters, call it without a template parameter.
- Overriding 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 parameters.
- When overriding a function with parameters pack, prefer to override with the same parameter pack.
Aucun commentaire:
Enregistrer un commentaire