A C++14 dependency injection framework without dynamic allocations.
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)
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>();
}