// Overview / Examples / API / FAQ
https://en.wikipedia.org/wiki/Dependency_injection (for additional info see FAQ)
- Single header (https://raw.githubusercontent.com/qlibs/di/main/di - for integration see FAQ)
- Verifies itself upon include (can be disabled with
-DNTEST
- see FAQ) - Minimal API
- Unified way for different polymorphism styles (
inheritance, type erasure, variant, ...
) - Constructor deduction for classes and aggregates
- Constructor order and types changes agnostic (simplifies integration with
third party
libraries) - Testing (different bindigns for
production
andtesting
,faking
some parameters withassisted
injection) - Policies (APIs with
checked
requirements) - Logging/Profiling/Serialization/... (via iteration over all
created
objects)
- Unified way for different polymorphism styles (
- C++20 (clang++13+, g++11+)
struct aggregate1 { int i1{}; int i2{}; };
struct aggregate2 { int i2{}; int i1{}; };
struct aggregate { aggregate1 a1{}; aggregate2 a2{}; };
// di::make (basic)
{
static_assert(42 == di::make<int>(42));
static_assert(aggregate1{1, 2} == di::make<aggregate1>(1, 2));
}
// di::make (generic)
{
auto a = di::make<aggregate1>(di::overload{
[](di::trait<std::is_integral> auto) { return 42; }
});
assert(a.i1 == 42);
assert(a.i2 == 42);
}
// di::make (assisted)
{
struct assisted {
constexpr assisted(int i, aggregate a, float f) : i{i}, a{a}, f{f} { }
int i{};
aggregate a{};
float f{};
};
auto fakeit = [](auto t) { return decltype(t.type()){}; };
auto a = di::make<assisted>(999, di::make<aggregate>(fakeit), 4.2f);
assert(a.i == 999);
assert(a.a.a1.i1 == 0);
assert(a.a.a1.i2 == 0);
assert(a.a.a2.i1 == 0);
assert(a.a.a2.i2 == 0);
assert(a.f == 4.2f);
}
// di::make (with names)
{
auto a = di::make<aggregate1>(di::overload{
[](di::is<int> auto t) requires (t.name() == "i1") { return 4; },
[](di::is<int> auto t) requires (t.name() == "i2") { return 2; },
});
assert(a.i1 == 4);
assert(a.i2 == 2);
}
// di::make (with names) - reverse order
{
auto a = di::make<aggregate2>(di::overload{
[](di::is<int> auto t) requires (t.name() == "i1") { return 4; },
[](di::is<int> auto t) requires (t.name() == "i2") { return 2; },
});
assert(a.i1 == 4);
assert(a.i2 == 2);
}
// di::make (with names, context and compound types)
{
auto a = di::make<aggregate>(di::overload{
// custom bindigs
[](di::trait<std::is_integral> auto t)
requires (t.name() == "i1" and
&typeid(t.parent().type()) == &typeid(aggregate1)) { return 99; },
[](di::trait<std::is_integral> auto) { return 42; },
// generic bindings
[](auto t) -> decltype(auto) { return di::make(t); }, // compund types
});
assert(a.a1.i1 == 99);
assert(a.a1.i2 == 42);
assert(a.a2.i1 == 42);
assert(a.a2.i2 == 42);
}
constexpr auto generic = di::overload{
[](auto t) -> decltype(auto) { return di::make(t); }, // compund types
};
// di::make (seperate overloads)
{
constexpr auto custom = di::overload {
[](di::trait<std::is_integral> auto t)
requires (t.name() == "i1" and
&typeid(t.parent().type()) == &typeid(aggregate1)) { return 99; },
[](di::trait<std::is_integral> auto t) { return decltype(t.type()){}; },
};
auto a = di::make<aggregate>(di::overload{custom, generic});
assert(a.a1.i1 == 99);
assert(a.a1.i2 == 0);
assert(a.a2.i1 == 0);
assert(a.a2.i2 == 0);
}
// di::make (polymorphism, scopes)
{
struct interface {
constexpr virtual ~interface() noexcept = default;
constexpr virtual auto fn() const -> int = 0;
};
struct implementation : interface {
constexpr implementation(int i) : i{i} { }
constexpr auto fn() const -> int override final { return i; }
int i{};
};
struct example {
example(
aggregate& a,
const std::shared_ptr<interface>& sp
) : a{a}, sp{sp} { }
aggregate a{};
std::shared_ptr<interface> sp{};
};
auto i = 123;
auto bindings = di::overload{
generic,
[](di::is<interface> auto t) { return di::make<implementation>(t); },
[&](di::is<int> auto) -> decltype(auto) { return i; }, // instance
// scopes
[](di::trait<std::is_reference> auto t) -> decltype(auto) {
using type = decltype(t.type());
static auto singleton{di::make<std::remove_cvref_t<type>>(t)};
return (singleton);
},
};
auto e = di::make<example>(bindings);
assert(123 == e.sp->fn());
assert(123 == e.a.a1.i1);
assert(123 == e.a.a1.i2);
assert(123 == e.a.a2.i1);
assert(123 == e.a.a2.i2);
// testing (override bindings)
{
auto testing = di::overload{
[](di::trait<std::is_integral> auto) { return 1000; }, // priority
[bindings](auto t) -> decltype(auto) { return bindings(t); }, // otherwise
};
auto e = di::make<example>(testing);
assert(1000 == e.sp->fn());
assert(1000 == e.a.a1.i1);
assert(1000 == e.a.a1.i2);
assert(1000 == e.a.a2.i1);
assert(1000 == e.a.a2.i2);
}
// logging
{
constexpr auto logger = [root = false]<class T, class TIndex, class TParent>(
di::provider<T, TIndex, TParent>&& t) mutable -> decltype(auto) {
if constexpr (constexpr auto is_root =
di::provider<T, TIndex, TParent>::size() == 1u; is_root) {
if (not std::exchange(root, true)) {
std::clog << reflect::type_name<decltype(t.parent().type())>() << '\n';
}
}
for (auto i = 0u; i < di::provider<T, TIndex, TParent>::size(); ++i) {
std::clog << ' ';
}
if constexpr (di::is_smart_ptr<std::remove_cvref_t<T>>) {
std::clog << reflect::type_name<T>() << '<'
<< reflect::type_name<
typename std::remove_cvref_t<T>::element_type>() << '>';
} else {
std::clog << reflect::type_name<T>();
}
if constexpr (not di::is_smart_ptr<std::remove_cvref_t<T>> and
requires { std::clog << std::declval<T>(); }) {
std::clog << ':' << t(t);
}
std::clog << '\n';
return t(t);
};
(void)di::make<example>(di::overload{logger, bindings});
// example
// aggregate
// aggregate1
// int:123
// int:123
// aggregate2
// int:123
// int:123
// shared_ptr<interface> -> implmentation
// int:123
}
}
// policies
{
struct policy {
constexpr policy(int*) { }
};
[[maybe_unused]] auto p = di::make<policy>(di::overload{
[]([[maybe_unused]] di::trait<std::is_pointer> auto t) {
static_assert(not sizeof(t), "raw pointers are not allowed!");
},
[](auto t) -> decltype(auto) { return di::make(t); }, // compund types
}); // error
}
// errors
{
(void)di::make<aggregate1>(di::overload{
// [](di::is<int> auto) { return 42; }, // missing binding
[](auto t) { return di::make(t); },
}); // di::error<int, ...>
}
// and more (see API)...
DIY - Dependency Injection Yourself (https://godbolt.org/z/acE3rYar5)
namespace di {
inline constexpr auto injector = [](auto&&... ts) {
return di::overload{
std::forward<decltype(ts)>(ts)...,
[](di::trait<std::is_reference> auto t) -> decltype(auto) {
using type = decltype(t.type());
static auto singleton{di::make<std::remove_cvref_t<type>>(t)};
return (singleton);
},
[](auto t) { return di::make(t); },
};
};
template<class T, class R = void>
inline constexpr auto bind = [] {
if constexpr (std::is_void_v<R>) {
return [](T&& to) {
return [&](di::is<T> auto) -> decltype(auto) {
return std::forward<T>(to);
};
};
} else {
return [](di::is<T> auto t) { return di::make<R>(t); };
}
}();
} // namespace di
int main() {
auto injector = di::injector(
di::bind<interface, implementation>,
di::bind<int>(42)
);
auto e = di::make<example>(injector);
assert(42 == e.sp->fn());
assert(42 == e.a.a1.i1);
assert(42 == e.a.a1.i2);
assert(42 == e.a.a2.i1);
assert(42 == e.a.a2.i2);
}
Standard Template Library (https://godbolt.org/z/jjbnffKne)
struct STL {
STL(std::vector<int> vector,
std::shared_ptr<void> shared_ptr,
std::unique_ptr<int> unique_ptr,
std::array<int, 42> array,
std::string string)
: vector(vector)
, shared_ptr(shared_ptr)
, unique_ptr(std::move(unique_ptr))
, array(array)
, string(string)
{ }
std::vector<int> vector;
std::shared_ptr<void> shared_ptr;
std::unique_ptr<int> unique_ptr;
std::array<int, 42> array;
std::string string;
};
int main() {
auto stl = di::make<STL>(
di::overload{
[](di::is<std::vector<int>> auto) { return std::vector{1, 2, 3}; },
[](di::is<std::shared_ptr<void>> auto) { return std::make_shared<int>(1); },
[](di::is<std::unique_ptr<int>> auto) { return std::make_unique<int>(2); },
[](di::is<std::array<int, 42>> auto) { return std::array<int, 42>{3}; },
[](di::is<std::string> auto) { return std::string{"di"}; },
[](auto t) { return di::make(t); },
}
);
assert(3u == stl.vector.size());
assert(1 == stl.vector[0]);
assert(2 == stl.vector[1]);
assert(3 == stl.vector[2]);
assert(1 == *static_cast<const int*>(stl.shared_ptr.get()));
assert(2 == *stl.unique_ptr);
assert(3 == stl.array[0]);
assert("di" == stl.string);
}
is_structural
- https://eel.is/c++draft/temp.param#def:type,structural (https://godbolt.org/z/1Mrxfbaqb)
template<class T, auto cfg =
[](auto t) {
using type = std::remove_cvref_t<decltype(t.type())>;
if constexpr (requires { type{}; }) {
return type{};
} else {
return di::make(t);
}
}
> concept is_structural = requires { []<T = di::make<T>(cfg)>{}(); };
static_assert(is_structural<int>);
static_assert(not is_structural<std::optional<int>>);
struct s { s() = delete; };
static_assert(not is_structural<s>);
struct y { int i; };
static_assert(is_structural<y>);
struct n { private: int i; };
static_assert(not is_structural<n>);
struct c1 { constexpr c1(int) {} };
static_assert(is_structural<c1>);
struct c2 { constexpr c2(int, double) {} };
static_assert(is_structural<c2>);
struct c3 { constexpr c3(std::optional<int>) {} };
static_assert(not is_structural<c3>);
struct c4 { constexpr c4(auto...) {} };
static_assert(is_structural<c4>);
struct c5 { private: constexpr c5(auto...) {} };
static_assert(not is_structural<c5>);
namespace di::inline v1_0_5 {
/**
* @code
* struct c1 { c1(int) { } };
* static_assert(std::is_same_v<type_list<int>, di::ctor_traits<c1>::type>);
* #endcode
*/
template<class T, std::size_t N = 16u> struct ctor_traits {
template<class...> struct type_list{};
using type = type_list</* T constructor parameters (size = N..0)`)*/>;
[[nodiscard]] constexpr auto operator()(auto&&...) const -> T;
};
/**
* static_assert(di::invocable<decltype([]{})>);
* static_assert(di::invocable<decltype([](auto...){})>);
*/
template<class T> concept invocable;
/**
* @code
* static_assert(not di::is<int, const int>);
* static_assert(di::is<void, void>);
* @endcode
*/
template<class TLhs, class TRhs> concept is;
/**
* @code
* static_assert(not di::is_a<int, std::shared_ptr>);
* static_assert(di::is_a<std::shared_ptr<void>, std::shared_ptr>);
*/
template<class T, template<class...> class R> concept is_a;
/**
* @code
* static_assert(not di::is_smart_ptr<void>);
* static_assert(di::is_smart_ptr<std::unique_ptr<int>>);
*/
template<class T> concept is_smart_ptr;
/**
* @code
* static_assert(not di::trait<int, std::is_const>);
* static_assert(di::trait<const int, std::is_const>);
*/
template<class T, template<class...> class Trait> concept trait;
/**
* @code
* static_assert(42 == di::overload{
* [](int i) { return i; },
* [](auto a) { return a; }
* }(42));
* @endcode
*/
template<class... Ts> struct overload;
/**
* Injection context
*/
template<class T, class Index, class TParent>
struct provider {
using value_type = T;
using parent_type = TParent;
static constexpr auto index() -> std::size_t; // index of parent constructor
static constexpr auto parent() -> parent_type; // callee provider
static constexpr auto type() -> value_type; // underlying type
static constexpr auto size() -> std::size_t; // size of parents
#if defined(REFLECT)
static constexpr auto name() -> std::string_view; // member name
#endif
};
/**
* @code
* static_assert(42 == di::make<int>(42));
* static_assert(42 == di::make<int>(
* di::overload{
* [](di::is<int> auto) { return 42; }
* }
* ));
* @endcode
*/
template<class T>
[[nodiscard]] constexpr auto make(auto&&...);
} // namespace di
-
Dependency Injection?
Dependency Injection (DI) - https://en.wikipedia.org/wiki/Dependency_injection - it's a technique focusing on producing loosely coupled code.
struct no_di { constexpr no_di() { } // No DI private: int data = 42; // coupled }; struct di { constexpr di(int data) : data{data} { } // DI private: int data{}; };
- In a very simplistic view, DI is about passing objects/types/etc via constructors and/or other forms of parameter propagating techniques instead of coupling values/types directly (
Hollywood Principle - Don't call us we'll call you
). - The main goal of DI is the flexibility of changing what's being injected. It's important though, what and how is being injected as that influences how good (
ETC - Easy To Change
) the design will be - more about it here - https://www.youtube.com/watch?v=yVogS4NbL6U.
- In a very simplistic view, DI is about passing objects/types/etc via constructors and/or other forms of parameter propagating techniques instead of coupling values/types directly (
-
Manual vs Automatic Dependency Injection?
Depedency Injection doesnt imply using a library. Automatic DI requires a library and makes more sense for larger projects as it helps limitting the wiring mess and the maintenance burden assosiated with it.
struct coffee_maker { coffee_maker(); // No DI private: basic_heater heater{}; // coupled basic_pump pump{}; // coupled }; struct coffee_maker_v1 { coffee_maker(iheater&, ipump& pump); // DI private: iheater& heater; // not coupled ipump& pump; // not coupled }; struct coffee_maker_v2 { coffee_maker(std::shared_ptr<ipump>, std::unique_ptr<iheater>); // DI private: std::shared_ptr<ipump> pump; // not coupled std::unique_ptr<iheater> heater; // not coupled }; int main() { // Manual Dependency Injection { basic_heater heater{}; basic_pump pump{}; coffe_maker_v1 cm{heater, pump}; } { auto pump = std::make_shared<basic_pump>(); auto heater = std::make_unique<basic_heater>(); coffe_maker_v2 cm{pump, std::move(heater)}; // different wiring } // Automatic Dependency Injection auto wiring = di::overload{ [](di::is<iheater> auto) { return make<basic_heater>(); }, [](di::is<ipump> auto) { return make<basic_pump>(); }, }; { auto cm = di::make<coffee_maker_v1>(wiring); } { auto cm = di::make<coffee_maker_v2>(wiring); // same wiring } }
The main goal of automatic is to avoid design compromises in order to reduce the boilerplate code/minimize maintance burden/simplify testing.
-
How does it work?
DI
works by deducing constructor parameters and calling appropriate overload to handle them by leavaring concepts - https://eel.is/c++draft/temp.constr.order#def:constraint,subsumption. The following represents the most important parts of the library design.template<class B, class T> concept copy_or_move = std::is_same_v<B, std::remove_cvref_t<T>>; template<class B, std::size_t N> struct any { template<class T> requires (not copy_or_move<B, T>) operator T() noexcept(noexcept(bind<arg<B, N>, T>{})); template<class T> requires (not copy_or_move<B, T>) operator T&() const noexcept(noexcept(bind<arg<B, N>, T&>{})); template<class T> requires (not copy_or_move<B, T>) operator const T&() const noexcept(noexcept(bind<arg<B, N>, const T&>{})); template<class T> requires (not copy_or_move<B, T>) operator T&&() const noexcept(noexcept(bind<arg<B, N>, T&&>{})); };
template<class T, std::size_t N = 16u> constexpr auto ctor_traits() { return []<std::size_t... Ns>(std::index_sequence<Ns...>) { if constexpr (requires { T{any<T, Ns>{}...}; }) { return type_list<typename decltype(get(detail::arg<T, Ns>{}))::value_type...>{}; } else if constexpr (sizeof...(Ns)) { return ctor_traits<T, N - 1u>(); } else { return type_list{}; } }(std::make_index_sequence<N>{}); }
template<class... Ts> struct overload : Ts... { using Ts::operator()...; }; template<class... Ts> overload(Ts...) -> overload<Ts...>;
template<class T, class...> auto error(auto&&...) -> T; template<class T> constexpr auto make(invocable auto&& t) { return [&]<template<class...> class TList, class... Ts>(TList<Ts...>) { if constexpr (requires { T{t(provider<Ts>(t)...); }; }) { return T{t(provider<Ts>(t)...}; } else { return error<T>(t); } }(ctor_traits<T>()); };
-
How to disable running tests at compile-time?
When
-DNTEST
is defined static_asserts tests wont be executed upon include. Note: Use with caution as disabling tests means that there are no gurantees upon include that given compiler/env combination works as expected. -
How to integrate with CMake.FetchContent?
include(FetchContent) FetchContent_Declare( qlibs.di GIT_REPOSITORY https://github.com/qlibs/di GIT_TAG v1.0.5 ) FetchContent_MakeAvailable(qlibs.di)
target_link_libraries(${PROJECT_NAME} PUBLIC qlibs.di);
-
Acknowledgments
- "Dependency Injection - a 25-dollar term for a 5-cent concept" (video)
- "Law of Demeter: A Practical Guide to Loose Coupling" (video)
- "Clean Code: A Handbook of Agile Software Craftsmanship" (book)
- "The Pragmatic Programmer" (book)
- "Design Patterns" (book)
- "Test Driven Development: By Example" (book)
-
Similar projects?
boost-ext.di, google.fruit, kangaru, wallaroo, hypodermic, dingo