Skip to content

qlibs/di

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 

Repository files navigation

// Overview / Examples / API / FAQ

DI: Dependency Injection library

MIT Licence Version Build Try it online

https://en.wikipedia.org/wiki/Dependency_injection (for additional info see FAQ)

Features

  • 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 and testing, faking some parameters with assisted injection)
    • Policies (APIs with checked requirements)
    • Logging/Profiling/Serialization/... (via iteration over all created objects)

Requirements


Overview

API (https://godbolt.org/z/xrzsYG1bj)

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)...

Examples

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>);

API

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

FAQ

  • 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.
  • 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

  • Similar projects?

    boost-ext.di, google.fruit, kangaru, wallaroo, hypodermic, dingo