Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

A Miniature IOC Container in C++

5.00/5 (1 vote)
5 Mar 2021CPOL2 min read 18.3K  
Miniature IOC container
Inversion of control containers allow systems to achieve dependency injection, by specifying the dependencies of objects indirectly. Typically, the IOC container will be responsible for the actual instantiation of a concrete type.

Introduction

Inversion of control containers allow systems to achieve dependency injection, by specifying the dependencies of objects indirectly. Typically, the IOC container will be responsible for the actual instantiation of a concrete object, selecting a constructor and returning only the reference type, obtaining instances of any dependencies before calling the constructor. Today, I’ve had a play at creating a minimal one of these and thought I would share it with you.

Obtaining a Type Code

Before we can safely store a way of obtaining a class subscribing to a particular interface, we should allocate a type code for this. In my implementation of the IOC container, I have a static integer variable that specifies the next type id that will be allocated, and a static local variable instance for each type, which can be accessed by calling the GetTypeID method.

C++
class IOCContainer
{
   static int s_nextTypeId;
public:

   //one typeid per type
   template<typename T>
   static int GetTypeID()
   {
      static int typeId = s_nextTypeId ++;
      return typeId;
   }

Obtaining Object Instances

Ok, that was easy. Now since we’ve got a type ID, we should be able to store some kind of factory object to represent the fact that we don’t know how to create the object. Since I want to store all of the factories in the same collection, I’m opting for an abstract base class which the factories will derive from, and an implementation that captures a functor to call later.

For brevity, I’ve used a std::map to hold the factories, however you might consider other options for the sake of efficiency. If you’re building the collection once and then using it several times, then a sorted std::vector of std::pair would make a lot of sense.

C++
class FactoryRoot
{
public:
   virtual ~FactoryRoot() {}
};

//todo: consider sorted vector
std::map<int, std::shared_ptr<FactoryRoot>> m_factories;

template<typename T>
class CFactory: public FactoryRoot
{
   std::function<std::shared_ptr<T> ()> m_functor;
public:
   ~CFactory() {}

   CFactory(std::function<std::shared_ptr<T> ()> functor)
      :m_functor(functor)
   {
   }

   std::shared_ptr<T> GetObject()
   {
      return m_functor();
   }
};

template<typename T>
std::shared_ptr<T> GetObject()
{
   auto typeId = GetTypeID<T>();
   auto factoryBase = m_factories[typeId];
   auto factory = std::static_pointer_cast<CFactory<T>>(factoryBase);
   return factory->GetObject();
}

Registering Instances

Now all we need to do is populate the collection. I’ve implemented a few different ways of doing this: You might want to supply a functor explicitly, or you might want to choose between a single instance, or creating new instances on demand.

C++
   //Most basic implementation - register a functor
   template<typename TInterface, typename ...TS>
   void RegisterFunctor(std::function<std::shared_ptr<TInterface> 
        (std::shared_ptr<TS> ...ts)> functor)
   {
      m_factories[GetTypeID<TInterface>()] = 
      std::make_shared<CFactory<TInterface>>([=]{return functor(GetObject<TS>()...);});
   }

   //Register one instance of an object
   template<typename TInterface>
   void RegisterInstance(std::shared_ptr<TInterface> t)
   {
      m_factories[GetTypeID<TInterface>()] = 
                  std::make_shared<CFactory<TInterface>>([=]{return t;});
   }

   //Supply a function pointer
   template<typename TInterface, typename ...TS>
   void RegisterFunctor(std::shared_ptr<TInterface> (*functor)(std::shared_ptr<TS> ...ts))
   {
      RegisterFunctor(std::function<std::shared_ptr<TInterface> 
                     (std::shared_ptr<TS> ...ts)>(functor));
   }
   
   //A factory that will call the constructor, per instance required
   template<typename TInterface, typename TConcrete, typename ...TArguments>
   void RegisterFactory()
   {
      RegisterFunctor(
         std::function<std::shared_ptr<TInterface> (std::shared_ptr<TArguments> ...ts)>(
            [](std::shared_ptr<TArguments>...arguments) -> std::shared_ptr<TInterface>
            {
               return std::make_shared<TConcrete>
               (std::forward<std::shared_ptr<TArguments>>(arguments)...);
            }));
   }
   
   //A factory that will return one instance for every request
   template<typename TInterface, typename TConcrete, typename ...TArguments>
   void RegisterInstance()
   {
      RegisterInstance<TInterface>(std::make_shared<TConcrete>(GetObject<TArguments>()...));
   }
};

Usage Scenario

Ok, we’re pretty much ready to go. Here, I’m going to show a toy usage scenario, where I’ve registered two objects – one with a constructor that will be called and pass in the instance of the other type. You’ll note that I have to define the static type id counter here, and give it an initial value – it’s not important what value you use here, but I just wanted it to be something that is easy to recognise.

C++
IOCContainer gContainer;

//initialise with nonzero number
int IOCContainer::s_nextTypeId = 115094801;

class IAmAThing
{
public:
   virtual ~IAmAThing() { }
   virtual void TestThis() = 0;
};

class IAmTheOtherThing
{
public:
   virtual ~IAmTheOtherThing() { }
   virtual void TheOtherTest() = 0;
};

class TheThing: public IAmAThing
{
public:
   TheThing()
   {
   }
   void TestThis()
   {
      std::cout << "A Thing" << std::endl;
   }
};

class TheOtherThing: public IAmTheOtherThing
{
   std::shared_ptr<IAmAThing> m_thing;
public:
   TheOtherThing(std::shared_ptr<IAmAThing> thing):m_thing(thing)
   {
   }
   void TheOtherTest()
   {
      m_thing->TestThis();
   }
};

int main(int argc, const char * argv[])
{
   gContainer.RegisterInstance<IAmAThing, TheThing>();
   gContainer.RegisterFactory<IAmTheOtherThing, TheOtherThing, IAmAThing>();
   gContainer.GetObject<IAmTheOtherThing>()->TheOtherTest();
   
   return 0;
}

Final Bits

So that’s pretty much it for this time. I think there’s potentially several things that you could do to improve the container, but this is a start. If you have any comments, suggestions, or complaints, let me know! I’d love to hear from you. Thanks for reading!

History

  • 13th September, 2015: Initial version
  • 6th March, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)