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...
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!
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.
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!
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!
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!
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!
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!
Check out also.
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.
See also.
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.
Check out also...
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!
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!
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!
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.
See also.
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.
See also.
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
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!