What’s in your DI Container, C++? Trying to write

And he was right, since reflection in C ++ has not yet been delivered. Although there is reflection ts and there was even an article on Habré, but so far it has not become part of the standard. I would like a library to grow out of this for creating dynamic reflection, turned off by default, turned on by attributes. But these are all dreams.

So what was found?

Frameworks:

Several articles and videos:

These containers have certain disadvantages. fruits, for example, implies the use of macros on the constructor of the class being injected. Wallaroo uses templates on class fields. Kangaroo already uses external service classes to bind dependencies than binds them statically. Hypodermic already looks solid, and the code on the github is nice and readable. Boost.DI… well… I tried to look source

All this was rather cumbersome for me, I wanted my own and simple, understandable like a bicycle … and so I began to write.

Before starting the journey

At the beginning, I somehow asked the question: “What do I want from the container?”. And I made some demands for myself:

  • Small volume and relative simplicity of the code.

  • No need to interfere with the design of the class to which the dependencies will be passed.

  • No macros. It’s not that I don’t like them. They have some cool features, but as a user of the library, I wouldn’t want a third party library to litter my IDE hints. Given C++’s fondness for incorporating features from other languages, Rust-style macros may someday be pulled up.

  • Use different smart and not so pointers and links.

  • Building a container Not during compilation.

At first I wanted to try to inject any types into the constructor, not just pointers, but this immediately creates problems.

Container base

The basis of DI containers in C++ is, of course, reflection templates. Hypodermic manages to use them to infer the types of arguments for the constructor. Of course, there are metadata generation tools in the C++ world, like Qt’s moc or UnrealEngine’s UnrealHeaderTool. It’s just that these are all third-party tools, and people don’t always want to depend on them.

In order to create an object, you must define the argument types for the constructor. And what’s the problem, if we exclude the lack of reflection in the pros? A constructor is not a normal function. If there is a fairly simple way for a function to get its arguments, then with a constructor this method is not suitable.

Hypodermic does this with a few helper template classes. Code below from Hypodermic itself

struct ArgumentResolverInvoker
{

  template <class T, class = /*scary code*/>
  operator T()
  {
      return ArgumentResolver< typename std::decay< T >::type >::template resolve(m_registration, m_resolutionContext);
  }
}

The compiler tries to infer the type of the cast operator for the ArgumentResolverInvoker and thereby tells the type of the constructor argument, and there are a few more classes behind that to help with that.

I chose the path through functions, which automatically means the need to write a factory method. Yes, in general, it is good. I saw this method in one of booksthere was an example of the implementation of fast delegates on the pluses.

template<class FunctionType> struct Factory;

template<class Type, class ... Args>
struct Factory<Type(Args...)>
{
  static void* Create(Container* container)
  {
    return TypeTraits<Type>::Create(container->resolve<Args>()...);
  }
}

This method uses is based on the fact that the compiler tries to use as highly specialized version of the template as possible, and when we write
Factory::Create)>
The compiler will go to the second version and infer all the required types for us.

TypeTraits is a helper template that will need to be specialized for each type we want to add to the container. It will need to contain a Create method that replicates the constructor signature of the type being created.
The code is approximate and will not work just like that. For example, Type will most likely need to be cleared from the pointer.

But describing the Create method each time is a little… sad. But here again we can get out of the pattern

template<class T, class... Args>
struct Constructor
{
  T* Create(Args... args)
  {
    return new T(std::forward<Args>(args)...);
  }
};


template<>
struct TypeTraits<Foo> : Constructor<Foo, Dependency1...>
{
  // TypeTraits не только для Create существует
  constexpr LifeTimeScope LifeTime = LifeTimeScope::Singleton;
  // ...
};

A separate Create method in TypeTraits and TypeTraits itself allows for many useful things:

  1. Classes from third-party libraries can be placed in a container, since there is no need to change them by defining TypeTraits.

  2. Use custom allocators in Create

  3. Call additional post-initialization methods.

However, I guess that the mentioned DI frameworks can allow you to do the same.

Finding the Id for the type

Okay, we have a factory class. Next, we need to register this class in the container. To do this, you just need to take the address of Factory::Create and put it in map. Then, when we want to get an object of the type we need, we get the method from the map and create an object. How do we create it? After all, the template argument of the Resolve method is a compile-time object, and map is a run-time object: one does not just go into the other. To do this, we will use another auxiliary template method.

template<class T>
size_t GetTypeId()
{
  return reintepret_cast<size_t>(&GetTypeId);
}

Yes, we will use the address of the template function as a key in the map. Due to the fact that each function will have its own memory location, the uniqueness of id will be achieved. I originally came up with the idea of ​​using the address of the Create method for this purpose, but if we want to map an interface type to an implementation type of that interface, we might not have a Create method for an interface.

Lifetime Management and Pointer Storage

Almost all the rest of the container code is a matter of technology. The next questions that may arise are, for example, how to deal with the lifetime.
I have identified 3 types:

  1. Singleton – lives all the time while the container and clients are alive.

  2. Reference counting – an object exists as long as the clients using this object are alive

  3. No way – the client himself decides what to do with it.

The first 2 differ in which smart pointer to store in the container: shared_ptr or weak_ptr, and what can be passed to the client. Passing a reference while having a weak_ptr in the container, as you might guess, will lead to problems.

And how to store pointers in the container? For this, I decided to use std:: variant. Thanks to him, you can bring all the pointers into one. Also, I use shared_ptr and so on. to store pointers. Despite being a little bad form, this allowed me to write a generic non-template Resolve method, and leave in the template only what really needed it.

Casting to the type required by the client

When we call the Resolve method on the factory we get the type of the argument

static void* Create(Container* container)
{
  return TypeTraits<Type>::Create(container->resolve<Args>()...);
}

And it’s likely that the type will be some kind of shared_ptr or Dependency&, but the Dependency is registered in the container, not one of those two.

So we say, “More templates for the template god” and move on.

template<class T, class ... Args>
using Reference = T&;

template<class T, class ... Args>
using SharedPtr = std::shared_ptr<T>;

template<class T>
struct WrapperInfo { };

template<class T>
struct WrapperInfo<T&>
{
    using Type = T;

    template<class P, class ... PArgs>
    using Wrapper = Reference<P>;
};

template<class T, template <class P, class ... PArgs> class TWrapper, class ... TArgs>
struct WrapperInfo<TWrapper<T, TArgs...>>
{
    using Type = T;

    template<class P, class ... PArgs>
    using Wrapper = TWrapper<P>;
};

Again, we use partial template specialization to find out what type we have in the original. And we can rewrite the Create method in accordance with the following code

static void* Create(Container* container)
{
  return TypeTraits<Type>::Create(
    container->resolve<typename WrapperInfo<Args>::Type, WrapperInfo<Args>::template Wrapper>()...
  );
}

Now the Resolve method knows what and in what form it needs to return.

Completion

Only about 500 lines of code, and there is a fully working container. I posted the code of my mini DI container on github. Although it cannot be compared with serious frameworks, I hope that this basic functionality will be enough in my further developments.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *