In my last blog post, I wrote about how I am automatically deducing constructor parameters in my dependency injection container. The approach had a major drawback: It worked only for 2 or more parameters, since there was an ambiguity with copy- or move-constructors with exactly one parameter.
Right after I wrote that post, I actually found a solution to that problem in the Boost.DI FAQ, which explains how to do that in its CPPnow 2016 slides. It restricts the type conversion operator by using SFINEA on an unused template parameter. I did not even know that was possible! It defines the templated conversion operator very similar to this:
template <class T,
class = std::enable_if_t<!std::is_same<std::remove_cvref_t<T>, Exclude>{}>>
operator T&() const
{
return p_->get<std::remove_cvref_t<T>>();
}
Since this is a bit more involved than the bare templated conversion operator from last time, repeating it would be bad. In the last version, I used 3 helper types, the inferred_locator
, mimic
and the provider_wrapper
, but that can be streamlined into one class:
template <typename Exclude> struct mimic
{
mimic(std::size_t)
{
}
mimic(service_provider const& p, std::size_t)
: p_(&p)
{
}
template <class T, class = std::enable_if_t<!std::is_same<std::remove_cvref_t<T>, Exclude>{}>> operator T&() const
{
return p_->get<std::remove_cvref_t<T>>();
}
service_provider const* p_{ nullptr };
};
Note that is uses some unused extra size_t
parameters, which make the parameter expansion easier in the next step. Now can use that for the SFINEA in the recursive construction:
// Actual dependency injection..
template <class T, std::size_t Head, std::size_t... Rest> constexpr auto
make_injected_(service_provider const& p, std::index_sequence<Head, Rest...>,
decltype(T{ mimic<T>{ Head }, mimic<T>{ Rest }... }) * = nullptr)
{
return std::make_unique(mimic<T>(p, Head), mimic<T>(p, Rest)...);
}
// Trivial no-dependency case
template <class T> constexpr auto
make_injected_(service_provider const& p, std::index_sequence<>)
{
return std::make_unique<T>();
}
// Fallback to try with fewer parameters
template <class T, std::size_t... Rest> constexpr auto make_injected_(service_provider const& p, std::index_sequence<Rest...>)
{
return make_injected_<T>(p, std::make_index_sequence<sizeof...(Rest) - 1>{});
}
template <class T, std::size_t Max = 16> auto
make_injected(service_provider const& p)
{
return make_injected_<T>(p, std::make_index_sequence<Max>{});
}
Just after I found this solution, my former colleague Dirk Reinbach sent me a very neat C++20 variant to restrict the conversion operator via a concept:
template <typename T, typename U>
concept not_is_same = !std::is_same_v<std::remove_cvref_t<T>, std::remove_cvref_t<U>>;
template <typename Exclude> struct mimic
{
/* other members... */
template <not_is_same<Exclude> T> operator T&() const
{
return p_->get<std::remove_cvref_t<T>>();
}
};
This works just as well, and is more readable, too. I have not measured, but I guess it’s probably also faster to compile, since all things SFINEA are notoriously slow.