I’m currently mostly switching back and forth between C# and C++ projects. One of the things that I’m missing most when switching to C++ is a nice dependency-injection (DI) library. After checking out what was already available, I finally decided I wanted to try to build my own slim type-indexed variant. I quickly started by registering factories and instances in a map on std::type_index
, making it possible to both have the DI retain ownership (with std::unique_ptr
) or just make a type available via a bare pointer. So I was able to do things like:
// Register an instance
di.insert_unique(std::make_unique<foo_service>());
// Register a factory
di.insert_unique([] {return std::make_unique<bar_service>());
// Register an existing bare pointer
di.insert_bare(my_bare_thingy);
// ... and retrieve them
auto& foo = di.get<foo_service>();
One of the most powerful aspects of a DI library is the ability to transitively setup dependencies. I like constructor injection the most, so I implemented a very naive way like this:
di.insert_unique([](auto& p) { return std::make_unique<complex_service>(
p.get<base_service1>(), p.get<base_service2>(), p.get<base_service3>());
});
This is pretty verbose and we basically have to repeat all the constructor parameter types. But it’s easy to implement. We can do a little bit better by using a templated type-conversion operator and using it to call the get:
class service_provider
{
struct inferred_locator
{
service_provider const* provider;
template <class T> operator T&() const
{
return provider->get<std::remove_const_t<T>>();
}
};
inferred_locator get() const
{
return { .provider = this };
}
/** typed get implementations here... */
};
Now we can reduce the previous registration to:
di.insert_unique([](auto& p) {
return std::make_unique<complex_service>(p.get(), p.get(), p.get());
});
That is basically only the number of constructor parameters in a verbose way. We could write a small template that takes the number, creates an std::index_sequence
from it and then unpacks each index into an invokation of service_provider::get
. But then we would still have to update registrations when adding (or removing) a new dependency to a services’s constructor. With a litte more work, we can actually get this instead:
di.insert_unique<complex_service>();
This partly inspired by Antony Polukhin’s C++ reflection talks, and combines std::index_sequence
based unpacking, SFINEA and the templated type-conversion operator:
template <class T, std::size_t Head, std::size_t... Rest>
constexpr auto make_unique_impl(provider_wrapper const& p,
std::index_sequence<Head, Rest...>,
decltype(T{ mimic{ Head }, mimic{ Rest }... }) * = nullptr) -> std::unique_ptr<T>
{
// This next requirement is so we do not accidentally recurse into the copy/move-ctors
static_assert(sizeof...(Rest) + 1 > 1, "Can only deduce constructors with two or more parameters.");
return std::make_unique<T>(p(Head), p(Rest)...);
}
template <class T, std::size_t... Rest>
constexpr auto make_unique_impl(provider_wrapper const& p, std::index_sequence<Rest...>) -> std::unique_ptr<T>
{
// This next requirement is so we do not accidentally recurse into the copy/move-ctors
static_assert(sizeof...(Rest) > 1, "Can only deduce constructors with two or more parameters.");
return make_unique_impl<T>(p, std::make_index_sequence<sizeof...(Rest) - 1>{});
}
template <class T, std::size_t Max = 8> auto make_unique(service_provider const& p)
{
return make_unique_impl<T>(provider_wrapper{ &p }, std::make_index_sequence<Max>{});
}
This uses two new types: mimic
, which is only used for SFINEA, takes std::size_t
on construction (for the unpacking from the std::index_sequence
) and converts to anything (templated type conversion again) and the provider_wrapper
, which is a simple adaptor around service_provider
that takes an unused std::size_t
argument (again, for unpacking). The first overload of make_unique_impl
is slightly more specialized (because it has Head and Rest), so the compiler tries it first. If it works, it returns a new instance of the service we want. Otherwise, it will fail without an error due to SFINEA in the unused and defaulted third parameter. The compiler will then try the second overload, which will recurse to a variant with fewer parameters. The outermost make_unique
starts this recursion with 8 parameters, because that should be enough for any sane service. I stop this recursion at one constructor parameter, even though that is a useful configuration. This is because I have not yet found a way to avoid calling the copy or move constructors accidentally. If anyone knows how to do that, I’d be very happy to hear how. My workaround right now is to explicitly register a factory in that case.
2 thoughts on “Automated instance construction in C++”