I am looking for an intuitive and extensible way to implement factories for subclasses of a given base class in c++. I want to provide such a factory function in a library.The tricky part is that I want said factory to work for user-defined subclasses as well (e.g. having the library's factory function build different subclasses depending on what modules are linked to it). The goal is to have minimal burden/confusion for downstream developers to use the factories.
An example of what I want to do is: given a std::istream
, construct and return an object of whatever subclass matches the content, or a null pointer if no matches are found. The global factory would have a signature like:
Base* Factory(std::istream &is){ ... };
I am familiar with prototype factories, but I prefer to avoid the need to make/store prototype objects. A related question is posted here for java: Allowing maximal flexibly/extensibility using a factory.
I am not looking for c++11-specific solutions at the moment, but if they are more elegant I would be happy to learn about those.
I came up with one working solution which I believe is fairly elegant, which I will post as an answer. I can imagine this problem to be fairly common, so I am wondering if anyone knows of better approaches.
EDIT: it seems some clarification is in order...
The idea is for the factory to construct an object of a derived class, without containing the logic to decide which one. To make matters worse, the factory method will end up as part of a library and derived classes may be defined in plugins.
Derived classes must be able to decide for themselves whether or not they are fit for construction, based on the input provided (for example an input file). This decision can be implemented as a predicate that can be used by the factory, as was suggested by several people (great suggestion, by the way!).
If I understand this correctly, we want a factory function that can select which derived class to instantiate based on constructor inputs. This is the most generic solution that I could come up with so far. You specify mapping inputs to organize factory functions, and then you can specify constructor inputs upon factory invocation. I hate to say that the code explains more than I could in words, however I think the example implementations of FactoryGen.h
in Base.h
and Derived.h
are clear enough with the help of comments. I can provide more details if necessary.
FactoryGen.h
#pragma once #include <map> #include <tuple> #include <typeinfo> //C++11 typename aliasing, doesn't work in visual studio though... /* template<typename Base> using FactoryGen<Base> = FactoryGen<Base,void>; */ //Assign unique ids to all classes within this map. Better than typeid(class).hash_code() since there is no computation during run-time. size_t __CLASS_UID = 0; template<typename T> inline size_t __GET_CLASS_UID(){ static const size_t id = __CLASS_UID++; return id; } //These are the common code snippets from the factories and their specializations. template<typename Base> struct FactoryGenCommon{ typedef std::pair<void*,size_t> Factory; //A factory is a function pointer and its unique type identifier //Generates the function pointer type so that I don't have stupid looking typedefs everywhere template<typename... InArgs> struct FPInfo{ //stands for "Function Pointer Information" typedef Base* (*Type)(InArgs...); }; //Check to see if a Factory is not null and matches it's signature (helps make sure a factory actually takes the specified inputs) template<typename... InArgs> static bool isValid(const Factory& factory){ auto maker = factory.first; if(maker==nullptr) return false; //we have to check if the Factory will take those inArgs auto type = factory.second; auto intype = __GET_CLASS_UID<FPInfo<InArgs...>>(); if(intype != type) return false; return true; } }; //template inputs are the Base type for which the factory returns, and the Args... that will determine how the function pointers are indexed. template<typename Base, typename... Args> struct FactoryGen : FactoryGenCommon<Base>{ typedef std::tuple<Args...> Tuple; typedef std::map<Tuple,Factory> Map; //the Args... are keys to a map of function pointers inline static Map& get(){ static Map factoryMap; return factoryMap; } template<typename... InArgs> static void add(void* factory, const Args&... args){ Tuple selTuple = std::make_tuple(args...); //selTuple means Selecting Tuple. This Tuple is the key to the map that gives us a function pointer get()[selTuple] = Factory(factory,__GET_CLASS_UID<FPInfo<InArgs...>>()); } template<typename... InArgs> static Base* make(const Args&... args, const InArgs&... inArgs){ Factory factory = get()[std::make_tuple(args...)]; if(!isValid<InArgs...>(factory)) return nullptr; return ((FPInfo<InArgs...>::Type)factory.first) (inArgs...); } }; //Specialize for factories with no selection mapping template<typename Base> struct FactoryGen<Base,void> : FactoryGenCommon<Base>{ inline static Factory& get(){ static Factory factory; return factory; } template<typename... InArgs> static void add(void* factory){ get() = Factory(factory,__GET_CLASS_UID<FPInfo<InArgs...>>()); } template<typename... InArgs> static Base* make(const InArgs&... inArgs){ Factory factory = get(); if(!isValid<InArgs...>(factory)) return nullptr; return ((FPInfo<InArgs...>::Type)factory.first) (inArgs...); } }; //this calls the function "initialize()" function to register each class ONCE with the respective factory (even if a class tries to initialize multiple times) //this step can probably be circumvented, but I'm not totally sure how template <class T> class RegisterInit { int& count(void) { static int x = 0; return x; } //counts the number of callers per derived public: RegisterInit(void) { if ((count())++ == 0) { //only initialize on the first caller of that class T T::initialize(); } } };
Base.h
#pragma once #include <map> #include <string> #include <iostream> #include "Procedure.h" #include "FactoryGen.h" class Base { public: static Base* makeBase(){ return new Base; } static void initialize(){ FactoryGen<Base,void>::add(Base::makeBase); } //we want this to be the default mapping, specify that it takes void inputs virtual void speak(){ std::cout << "Base" << std::endl; } }; RegisterInit<Base> __Base; //calls initialize for Base
Derived.h
#pragma once #include "Base.h" class Derived0 : public Base { private: std::string speakStr; public: Derived0(std::string sayThis){ speakStr=sayThis; } static Base* make(std::string sayThis){ return new Derived0(sayThis); } static void initialize(){ FactoryGen<Base,int>::add<std::string>(Derived0::make,0); } //we map to this subclass via int with 0, but specify that it takes a string input virtual void speak(){ std::cout << speakStr << std::endl; } }; RegisterInit<Derived0> __d0init; //calls initialize() for Derived0 class Derived1 : public Base { private: std::string speakStr; public: Derived1(std::string sayThis){ speakStr=sayThis; } static Base* make(std::string sayThat){ return new Derived0(sayThat); } static void initialize(){ FactoryGen<Base,int>::add<std::string>(Derived0::make,1); } //we map to this subclass via int with 1, but specify that it takes a string input virtual void speak(){ std::cout << speakStr << std::endl; } }; RegisterInit<Derived1> __d1init; //calls initialize() for Derived1
Main.cpp
#include <windows.h> //for Sleep() #include "Base.h" #include "Derived.h" using namespace std; int main(){ Base* b = FactoryGen<Base,void>::make(); //no mapping, no inputs Base* d0 = FactoryGen<Base,int>::make<string>(0,"Derived0"); //int mapping, string input Base* d1 = FactoryGen<Base,int>::make<string>(1,"I am Derived1"); //int mapping, string input b->speak(); d0->speak(); d1->speak(); cout << "Size of Base: " << sizeof(Base) << endl; cout << "Size of Derived0: " << sizeof(Derived0) << endl; Sleep(3000); //Windows & Visual Studio, sry }
I think this is a pretty flexible/extensible factory library. While the code for it is not very intuitive, I think using it is fairly simple. Of course, my view is biased seeing as I'm the one that wrote it, so please let me know if it is the contrary.
EDIT : Cleaned up the FactoryGen.h file. This is probably my last update, however this has been a fun exercise.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With