0. [Pre] Refactor towards DI

If you write a new application you can skip this step and go directly to step 1. However, if you have a lot code which is not using DI and you wonder what can it be refactored, then you are in the right place.

Basically, there is a only one (big) step to get all benefits of Dependency Injection. You have to separate creation logic from business logic, which means that your code should be free of object creation inside other objects...

class controller {
public:
  controller(config c) 
    : model_(std::make_unique<model>(c))
  { }

  void run();

private:
  std::unique_ptr<model> model_;
};

int main() {
  controller controller_;
  controller_.run();
}

Instead, DI approach would look like that...

class controller {
public:
  explicit controller(model& m) : model_(m) {}
  void run();

private:
  model& model_;
};

int main() {
  model model_;
  controller controller_{model_};
  controller_.run();
}

So, what happened here? We just took the responsibility of creation model out from the controller. In other words, we have split the creation logic and the business logic.

That's basically everything you have to remember in order to create applications using DI. Nevertheless, please, be careful and don't 'carry out' your dependencies. What is meant by that, is NOT to pass an object into constructor if it won't be stored (Law of Demeter).

class app {
public:
  explicit app(model& m) : controller_(m) {} // BAD
  explicit app(controller& c) : controller_(c) {} // GOOD

private:
  controller controller_;
};
class controller {
public:
  explicit controller(model&);
};

int main() {
  model model_;
  app app_{model_};
}

Additionally, you can consider using strong typedefs which will make your constructor interface cleaner/stronger.

class button {
public:
  button(int, int); // weak constructor interface (cpp file has to checked in order to figure out the meaning of int's)
};

button constructor is not clear because int's are ambiguous and both present just a number. It can be seen more clearly in the following example.

button{10, 15}; // OK, but what's 10? what's 15? Can I swap them?
button{15, 10}; // Hmm, potenial missue of the constructor

A better approach would be to introduce a strong typedefs for both numbers in order to avoid potential misuse of the constructor, especially when used by other/external teams.

struct width {
  int value;
  constexpr operator int() const { return value; }
};
struct height {
  int value;
  constexpr operator int() const { return value; }
};
class button {
public:
  button(width, height); // strong constructor interface
};

Right now, button constructor is much easier to follow (no need to check cpp file) because it expresses the intention.

button{width{10}, height{15}}; // OK, declartive approach
button{height{10}, with{15}}; // Compile Error
button{10, 15}; // Compile Error

Similar mechanism is used by [Boost].DI to achieve named parameters which and it will be presented in this tutorial later on.

1. [Basic] Create objects tree

Before we will get into creating objects tree, let's first create a 'dummy' example. In order to do so, firstly, we have to include (one and only) boost/di.hpp header

wget https://raw.githubusercontent.com/boost-ext/di/cpp14/include/boost/di.hpp

and declare a convenient di namespace alias.

#include <boost/di.hpp>
namespace di = boost::di;

That is enough to try out [Boost].DI!

To have a first complete and working example we just have to add main function as usual.

int main() {}

and compile our code using compiler supporting C++14 standard (Clang-3.4/GCC-5/MSVC-2015).

$CXX -std=c++14 example.cpp

Congrats, you are now ready to check out [Boost].DI features!


Let's move on to creating objects tree. Applications, usually, consists of a number of objects which have to be instantiated. For example, let's consider a simplified Model View Controller code...

Create objects tree

The usual approach to create app would be following...

renderer renderer_;
view view_{"", renderer_};
model model_;
controller controller_{model_, view_};
user user_;
app app_{controller_, user_};

Which is alright for a really small applications. However, it's really tedious to maintain. Just imagine, that we have to change something here. For instance, view may need a new object window or, even worse, we refactored the code and dependencies order has changed - yea ORDER of above is important! ANY change in these classes constructors require developer input to maintain above boilerplate code! Not fun, not fun at all :(

Right now imagine that your maintain effort will be minimized almost to none. How does it sound? Well, that might be simply achieved with [Boost].DI!

The same result might be achieved with [Boost].DI. All, non-ambiguous, dependencies will be automatically resolved and injected properly. It doesn't matter how big the hierarchy will be and/or if the order of constructor parameters will be changed in the future. We just have to create injector using make_injector, create the app and DI will take care of injecting proper types for us.

auto app_ = make_injector().create<app>(); // It will create an `app` on stack and call its copy constructor

How is that possible? [Boost].DI is able to figure out what parameters are required for the constructor of type T. Also, [Boost].DI is able to do it recursively for all required types by the constructor T. Hence, NO information about constructors parameters is required to be registered.

Moreover, changes in the constructor of created objects will be handled automatically, so in our case when we add a window to view or change view& to std::shared_ptr<view> required effort will be exactly '0'. [Boost].DI will take care of everything for us!

Type T Is allowed? Note
T -
T* Ownership transfer!
const T* Ownership transfer!
T& -
const T& Reference with singleton / Temporary with unique
T&& -
std::unique_ptr<T> -
std::shared_ptr<T> -
std::weak_ptr<T> -
boost_shared_ptr<T> -

Furthermore, there is no performance penalty for using [Boost].DI (see Performance)!

Note

[Boost].DI can inject dependencies using direct initialization T(...) or uniform initialization T{...} for aggregate types.

And full example! CPP

Check out also other examples. Please, notice that the diagram was also generated using [Boost].DI but we will get into that a bit later.

CPP(BTN) CPP(BTN) CPP(BTN)



2. [Basic] First steps with bindings

But objects tree is not everything. A lot of classes uses interfaces or required a value to be passed. [Boost].DI solution for this are bindings.

For purpose of this tutorial, let's change view class into interface iview in order to support text_view and gui_view.

class iview {
public:
  virtual ~iview() noexcept = default;
  virtual void update() =0;
};

class gui_view: public iview {
public:
  gui_view(std::string title, const renderer&) {}
  void update() override {}
};

class text_view: public iview {
public:
  void update() override {}
};

Please, notice that text_view doesn't require any constructor parameters, whilst gui_view does.

So, what will happen right now, when we try to create an app?

auto app = make_injector().create<app>();

COMPILE error! (See also: Error Messages)

warning: 'create<app>' is deprecated: creatable constraint not satisfied
  injector.create<app>();
           ^
boost/di.hpp:870:2: error: 'boost::di::v1_0_0::concepts::abstract_type<iview>::is_not_bound::error'
  error(_ = "type is not bound, did you forget to add: 'di::bind<interface>.to<implementation>()'?");

Note

You can get more info about error by increasing BOOST_DI_CFG_DIAGNOSTICS_LEVEL [0-2] value (default=1).

Ah, okay, we haven't bound iview which means that BOOST.DI can't figure out whether we want text_view or gui_view? Well, it's really simple to fix it, we just follow suggestion provided.

const auto injector = di::make_injector(
  di::bind<iview>.to<gui_view>()
);

Let's try again. Yay! It's compiling.

But what about render.device value? So far, it was value initialized by default(=0). What, if you we want to initialize it with a user defined value instead? We've already seen how to bind interface to implementation. The same approach might be used in order to bind a type to a value.

di::bind<T>.to(value) // bind type T to given value

Moving back to our render.device...

struct renderer {
  int device;
};

Note

If you want change the default behaviour and be sure that all required dependencies are bound and not value initialized take a look at constructible policy.

const auto injector = di::make_injector(
  di::bind<iview>.to<gui_view>()
, di::bind<int>.to(42) // renderer.device | [Boost].DI can also deduce 'int' type for you -> 'di::bind<>.to(42)'
);

Note

[Boost].DI is a compile time beast which means that it guarantees that if your code compiles, all dependencies will be resolved correctly. No runtime exceptions or runtime asserts, EVER!

And full example! CPP

That's nice but I don't want to be using a dynamic (virtual) dispatch. What about concepts/templates? Good news, [Boost].DI can inject concepts/templates too!

template <class T = class Greater>
struct example { 
  using type = T;
};

struct hello {};

int main() {
  const auto injector = di::make_injector(
    di::bind<class Greater>.to<hello>()
  );

  auto object = injector.create<example>();
  static_assert(std::is_same<hello, decltype(object)::type>{});
}

And full example! CPP

Great, but my code is more dynamic than that! I mean that I want to choose gui_view or text_view at runtime. [Boost].DI can handle that too!

auto use_gui_view = true/false;

const auto injector = di::make_injector(
  di::bind<iview>.to([&](const auto& injector) -> iview& {
    if (use_gui_view)
      return injector.template create<gui_view&>();
    else
      return injector.template create<text_view&>();
  })
, di::bind<>.to(42) // renderer device
);

Note

It is safe to throw exceptions from lambda. It will be passed through.

Notice, that injector was passed to lambda expression in order to create gui_view / text_view. This way [Boost].DI can inject appropriate dependencies into chosen types. See bindings for more details.

And full example! CPP

Okay, so what about the input. We have user, however, in the real life, we will have more clients. [Boost].DI allows multiple bindings to the same type for array/vector/set. Let's do it then!

class iclient {
 public:
   virtual ~iclient() noexcept = default;
   virtual void process() = 0;
};

class user : public iclient {
 public:
   void process() override {};
};

class timer : public iclient {
 public:
   void process() override {};
};

class app {
 public:
  app(controller&, std::vector<std::unique_ptr<iclient>>);
};

And our bindings...

di::bind<iclient*[]>.to<user, client>()

And full example! CPP

The last but not least, sometimes, it's really useful to override some bindings. For example, for testing purposes. With [Boost].DI you can easily do that with override specifier (Implemented using operator[](override)).

const auto injector = di::make_injector(
  di::bind<int>.to(42) // renderer device
, di::bind<int>.to(123) [di::override] // override renderer device
);

Without the di::override following compilation error will occur...

boost/di.hpp:281:3: error: static_assert failed "constraint not satisfied"
boost/di.hpp:2683:80: type<int>::is_bound_more_than_once
  inline auto make_injector(TDeps... args) noexcept

And full example! CPP

Check out also.

CPP(BTN) CPP(BTN) CPP(BTN) CPP(BTN) CPP(BTN)





3. [Basic] Decide the life times

So far so good but where are these objects stored? Well, [Boost].DI supports scopes which are response for maintaining the life time of created objects. By default there are 4 scopes

  • deduce scope (default)
  • instance scope (bind<>.to(value) where value is maintained by the user)
  • unique scope (one instance per request)
  • singleton scope (shared instance)

By default deduce scope is used which means that scope is deduced based on a constructor parameter. For instance, reference, shared_ptr will be deduced as singleton scope and pointer, unique_ptr will be deduced as unique scope.

Type Scope
T unique
T& singleton
const T& unique (temporary) / singleton
T* unique (ownership transfer)
const T* unique (ownership transfer)
T&& unique
std::unique_ptr unique
std::shared_ptr singleton
boost::shared_ptr singleton
std::weak_ptr singleton

Example

class scopes_deduction {
  scopes_deduction(const int& /*singleton scope*/,
                   std::shared_ptr<int> /*singleton scope*/,
                   std::unique_ptr<int> /*unique scope*/,
                   int /*unique scope*/)
  { }
};

di::make_injector().create<example>(); // scopes will be deduced based on constructor parameter types

Coming back to our example, we got quite a few singletons there as we just needed one instance per application life time. Although scope deduction is very useful, it's not always what we need and therefore [Boost].DI allows changing the scope for a given type.

const auto injector = di::make_injector(
  di::bind<iview>.to<gui_view>().in(di::singleton) // explicitly specify singleton scope
);

What if I want to change gui_view to be a different instance per each request. Let's change the scope to unique then.

const auto injector = di::make_injector(
  di::bind<iview>.to<gui_view>().in(di::unique)
);

We will get a COMPILATION TIME ERROR because a unique scope can't be converted to a reference. In other words, having a reference to a copy is forbidden and it won't compile!

warning: 'create<app>' is deprecated: creatable constraint not satisfied
  injector.create<app>();
           ^
boost/di.hpp:897:2: error: 'scoped<scopes::unique, gui_view>::is_not_convertible_to<iview &>::error'
  error(_ = "scoped object is not convertible to the requested type, did you mistake the scope: 'di::bind<T>.in(scope)'?");

Ah, reference doesn't make much sense with unique scope because it would mean that it has to be stored somewhere. It would be better to use std::unique_ptr<iview> instead.

Type/Scope unique singleton instance
T -
T& -
const T& ✔ (temporary)
T* (transfer ownership) - -
const T* - -
T&& -
std::unique_ptr - -
std::shared_ptr
boost::shared_ptr - / ✔ converted to
std::weak_ptr - - / ✔ converted to

Hmm, let's try something else then. We have list of unique clients, we can share objects just by changing the list to use std::shared_ptr instead.

class app {
 public:
  app(controller&, std::vector<std::shared_ptr<iclient>>);
};

But, it would be better if timer was always created per request, although it's a shared_ptr. To do so, we just need add scope when binding it, like this...

const auto injector = di::make_injector(
  di::bind<timer>.in(di::unique) // different per request
);

Check out the full example here. CPP

See also.

CPP(BTN) CPP(BTN) CPP(BTN)




4. [Basic] Annotations to the rescue

Above example are fine and dandy, nonetheless, they don't cover one important thing. How [Boost].DI knows which constructor to choose and what if they are ambiguous?

Well, the algorithm is very simple. The longest (most parameters), unique constructor will be chosen. Otherwise, [Boost].DI will give up with a compile time error. However, which constructor should be chosen is configurable by BOOST_DI_INJECT.

To illustrate this, let's modify model constructor.

class model {
 public:
   model(int size, double precision) { }
   model(int rows, int cols) { }
};

Right now, as expected, we get a compile time error!

warning: 'create<app>' is deprecated: creatable constraint not satisfied
  injector.create<app>();
           ^
boost/di.hpp:942:4: error: 'type<model>::has_ambiguous_number_of_constructor_parameters::error'
  error(_ = "verify BOOST_DI_INJECT_TRAITS or di::ctor_traits");

Let's fix it using BOOST_DI_INJECT then!

class model {
 public:
   model(int size, double precision) { }
   BOOST_DI_INJECT(model, int rows, int cols) { } // this constructor will be injected
};

Note

We can also write model(int rows, int cols, ...) to get the same result. By adding ... as the last parameter of the constructor it's guaranteed by [Boost].DI that it will be used for injection as it will have the highest number of constructor parameters (infinite number).

Okay, right now it compiles but, wait a minute, 123 (renderer device) was injected for both rows and cols! Well, it wasn't even close to what we wanted, but we can fix it easily using named annotations.

Firstly, we have to create names. That's easy as names are just unique objects.

auto Rows = []{};
auto Cols = []{};

Secondly, we have to tell model constructor about it.

class model {
 public:
   model(int size, double precision) { }
   BOOST_DI_INJECT(model, (named = Rows) int rows, (named = Cols) int cols); // this constructor will be injected
};

model::model(int rows, int cols) {}

Please, notice that we have separated model constructor definition and declaration to show that definition doesn't require named annotations.

Note

If you happen to use clang/gcc compiler you can use string literals instead of creating objects, for example (named = "Rows"_s).

Finally, we have to bind our values.

const auto injector = di::make_injector(
  di::bind<int>.named(Rows).to(6)
, di::bind<int>.named(Cols).to(8)
);

That's all.

Note

The same result might be accomplished with having different types for rows and cols.

Full example here. CPP

Check out also...

CPP(BTN) CPP(BTN) CPP(BTN)




5. [Basic] Split your configuration

But my project has hundreds of interfaces and I would like to split my bindings into separate components. This is simple to do with [Boost.DI] as an injector can be extended by other injectors.

Let's split our configuration then and keep our model bindings separately from app bindings.

auto model_module = [] {
  return di::make_injector(
    di::bind<int>.named(Rows).to(6)
  , di::bind<int>.named(Cols).to(8)
  );
};

auto app_module = [](const bool& use_gui_view) {
  return di::make_injector(
    di::bind<iview>.to([&](const auto& injector) -> iview& {
      if (use_gui_view)
        return injector.template create<gui_view&>();
      else
        return injector.template create<text_view&>();
    })
  , di::bind<timer>.in(di::unique) // different per request
  , di::bind<iclient*[]>().to<user, timer>() // bind many clients
  );
};

And glue them into one injector the same way...

  const auto injector = di::make_injector(
    model_module()
  , app_module(use_gui_view)
  );

Note

Gluing many injectors into one is order independent.

And full example! CPP

But I would like to have a module in cpp file, how can I do that? Such design might be achieved with [Boost].DI using injector and exposing given types.

  • Expose all types (default)
const const auto injector = // auto exposes all types
  di::make_injector(
    di::bind<int>.to(42)
  , di::bind<double>.to(87.0)
  );

injector.create<int>(); OK
injector.create<double>(); // OK
  • Expose only specific types
const di::injector<int> injector = // only int is exposed
  di::make_injector(
    di::bind<int>.to(42)
  , di::bind<double>.to(87.0)
  );

injector.create<int>(); OK
injector.create<double>(); // COMPILE TIME ERROR, double is not exposed by the injector

When exposing all types using auto modules have to be implemented in a header file. With di::injector<T...> a definition might be put in a cpp file as it’s just a regular type.

Such approach has a few benefits: * It’s useful for encapsulation (ex. Another team provides a module but they don't want to expose an ability to create implementation details) * May also speed compilation times in case of extend usage of cpp files

Note

There is no performance (compile-time, run-time) overhead between exposing all types or just a specific ones.

Moving back to our example. Let's refactor it then.

di::injector<model&> model_module() {
  return di::make_injector(
    di::bind<int>.named(Rows).to(6)
  , di::bind<int>.named(Cols).to(8)
  );
}

di::injector<app> app_module(const bool& use_gui_view) {
  return di::make_injector(
    di::bind<iview>.to([&](const auto& injector) -> iview& {
      if (use_gui_view)
        return injector.template create<gui_view&>();
      else
        return injector.template create<text_view&>();
    })
  , di::bind<timer>.in(di::unique) // different per request
  , di::bind<iclient*[]>.to<user, timer>() // bind many clients
  , model_module()
  );
}

Right now you can easily separate definition and declaration between hpp and cpp files.

Check the full example here! CPP

Note

You can also expose named parameters using di::injector<BOOST_DI_EXPOSE((named = Rows) int)>. Different variations of the same type have to be exposed explicitly using di::injector<model&, std::unique_ptr<model>>. Type erasure is used under the hood when types are exposed explicitly (di::injector<T…>).

Check out more examples here!

CPP(BTN) CPP(BTN) CPP(BTN)



Congrats! You have finished the basic part of the tutorial. Hopefully, you have noticed potential of DI and [Boost].DI but if are still not convinced check out the Advanced part.

6. [Advanced] Dump/Limit your types

It's often a case that we would like to generate object diagram of our application in order to see code dependencies more clear. Usually, it's a really hard task as creation of objects may happen anywhere in the code. However, if the responsibility for creation objects will be given to [Boost].DI we get such functionality for free. The only thing we have to do is to implement how to dump our objects.

Let's dump our dependencies using Plant UML format.

CPP UML Dumper

See also.

CPP(BTN) CPP(BTN)


On the other hand, it would be great to be able to limit types which might be constructed. For example, we just want to allow smart pointers and disallow raw pointers too. We may want to have a view only with const parameters being passed, etc. [Boost].DI allows you to do so by using constructible policy or writing a custom policy.

CPP

See also.

CPP(BTN)



7. [Advanced] Customize it

[Boost].DI was design having extensibility in mind. You can easily customize

  • scopes - to have custom life time of an object
  • providers - to have custom way of creating objects, for example by using preallocated memory
  • policies - to have custom way of dumping types at run-time or limiting them at compile-time

CPP(BTN) CPP(BTN) CPP(BTN)



8. [Advanced] Extend it

As mentioned before, [Boost].DI is quite easy to extend and therefore a lot of extensions exists already. Please check them out and write your own!