/staticdi

Static dependency injection framework for C++

Primary LanguageC++

staticdi

A C++14 dependency injection framework without dynamic allocations.

Including the framework in your project

The easiest way to include the project is using CMake's FetchContent feature:

include(FetchContent)
FetchContent_Declare(
    staticdi
    GIT_REPOSITORY https://github.com/Drako/staticdi.git
    GIT_TAG <version>
)
FetchContent_MakeAvailable(staticdi)

#...
target_link_libraries(<my-lib> PUBLIC staticdi)
target_link_libraries(<my-executable> PRIVATE staticdi)

Using the framework

The core of the framework is the sdi::container class.

For it to be able to resolve dependencies, it needs to know about all your concrete classes:

using app_container = sdi::container<
      sdi::known_types<A, B, C>
    >;

With that, the container knows about the classes A, B and C.

If C were to depend on A and B, this would be expressed in the following way:

class C final {
  A const& a;
  B const& b;
  
public:
  // The sdi::container checks for this.
  // If it is missing, it is assumed, that the class has no dependencies.
  using dependencies = sdi::dependencies<A, B>;
  
  // The constructor must be compatible with the dependencies declared above.
  C(A const& a, B const& b)
    : a{a}
    , b{b}
  {
  }
};

Now let's assume A is default constructible, so it has no dependencies. We don't need to do anything special.

But B has a dependency, that is outside of the scope of the container. It could be, that we want to express a dependency to a global object like std::cout.

We could do following:

class B final {
  std::ostream& out;
  
public:
  // Basically the same as above.
  // It should be noted, that std::ostream is an interface.
  using dependencies = sdi::dependencies<std::ostream>;
  
  // It is up to the developer, to decice,
  // whether a reference or a const reference is needed.
  B(std::ostream& out) : out{out}
  {
  }
};

Now that we have our classes, let's see how we can get an instance of C:

int main() {
  // this instance of the container lives in static memory,
  // but we could also create a local instance on the stack.
  // there are no dynamic allocations by the framework, though.
  auto& container = app_container::instance();
  
  // as std::cout is outside of the scope of the container,
  // we need to emplace an instance of B manually.
  container.emplace<B>(std::cout);
  
  // this will default construct an A,
  // find the previously emplaced B,
  // and finally construct a C, injecting the A and B instances.
  auto& c = container.resolve<C>();
}

For a test scenario we could want to use an std::ostringstream instead:

void test_function() {
  // this one lives on the stack
  sdi::container<A, B, C, std::ostringstream> test_container;
  
  // this will default construct an A,
  // default construct an std::ostringstream,
  // construct a B, injecting the std::ostringstream,
  // and finally construct a C, injecting the A and B instances.
  auto& c = test_container.resolve<C>();
  
  // let's say we wanted to check some output later on:
  auto& out = test_container.resolve<std::ostring_stream>();
  assert(out.str() == "foo");
}

Additionally, we can hand in global variables via std::reference_wrapper:

using app_container = sdi::container<A, B, C, std::reference_wrapper<std::ostream>>;

int main() {
  auto& container = app_container::instance();
  // after that, classes which depend on an std::ostream& would receive a reference to std::cout.
  container.emplace<std::reference_wrapper<std::ostream>>(std::cout);
  
  auto& c = container.resolve<C>();
}