An attribute-style dependency-injection container for modern C++.
This is a port of a DI container I've used in EmptySolutions, rebuilt for C++. A lot of the API is a direct translation of the original.
Di::Container::DiContainer container; // discovers every registered service
auto greeter = container.Provider().ResolveService<IGreeter>();
std::cout << greeter->Greet() << '\n';- Constructor injection — interfaces stay plain abstract classes, no DI annotations on them
- Four lifetimes:
Singleton,LazySingleton,Scoped,Transient - Dependency wrappers for the common cases: required, keyed, optional, collection, factory, lazy
- Keyed registrations and multiple implementations of one interface
- Decorators for cross-cutting concerns (logging, caching, retry), without touching the implementation or its callers
- Scopes with deterministic disposal in reverse creation order
- Construction-free validation of the whole graph, plus always-on cycle detection
- Module/anchor discovery that survives static-library boundaries
- A manual registration API for third-party types you can't annotate
An interface is a plain abstract class — it's never annotated. The descriptor goes on the implementation instead.
// IClock.hpp
struct IClock
{
virtual ~IClock() = default;
virtual std::string Now() const = 0;
};
// IGreeter.hpp
struct IGreeter
{
virtual ~IGreeter() = default;
virtual std::string Greet() const = 0;
};// SystemClock.hpp / Greeter.hpp — implementations
class SystemClock final : public IClock
{
public:
std::string Now() const override { return "12:00"; }
};
class Greeter final : public IGreeter
{
public:
explicit Greeter(std::shared_ptr<IClock> clock) : m_Clock(std::move(clock)) {}
std::string Greet() const override { return "Hello at " + m_Clock->Now(); }
private:
std::shared_ptr<IClock> m_Clock;
};Describe each implementation once, in its registration translation unit:
// AppModule.cpp
#include "Di/Registry/DiDescribe.hpp"
#include "Di/Registry/DiModuleAnchor.hpp"
DI_MODULE_ANCHOR(App)
DI_DESCRIBE(SystemClock)
{
DI_SERVICE(IClock);
DI_LIFETIME(Singleton);
DI_MODULE(App);
};
DI_DESCRIBE(Greeter)
{
DI_SERVICE(IGreeter);
DI_DEPENDS(Di::Service<IClock>); // constructor parameters, in order
DI_LIFETIME(Singleton);
DI_MODULE(App);
};Resolve from the composition root:
// main.cpp
#include "Di/Container/DiContainer.hpp"
DI_USE_MODULE(App)
int main()
{
Di::Container::DiContainer container;
auto greeter = container.Provider().ResolveService<IGreeter>();
std::cout << greeter->Greet() << '\n'; // Hello at 12:00
}Descriptor macros, for reference:
| Macro | Meaning | Default |
|---|---|---|
DI_SERVICE(I) |
interface the type is registered as | the type itself |
DI_DEPENDS(...) |
constructor parameters, as wrappers | none |
DI_EXPOSE(...) |
extra interfaces the type also answers for | none |
DI_LIFETIME(name) |
Singleton / LazySingleton / Scoped / Transient |
Transient |
DI_KEY("text") |
named registration | none |
DI_ORDER(n) |
ordering hint | 0 |
DI_MODULE(Name) |
self-register into module Name |
— |
Builds as a static library target, Di::DependencyInjection. Simplest integration is a subdirectory:
add_subdirectory(third_party/DependencyInjection)
target_link_libraries(my_app PRIVATE Di::DependencyInjection)Or with FetchContent:
include(FetchContent)
FetchContent_Declare(DependencyInjection
GIT_REPOSITORY https://github.com/Ukine9999/DependencyInjection.git
GIT_TAG main)
FetchContent_MakeAvailable(DependencyInjection)
target_link_libraries(my_app PRIVATE Di::DependencyInjection)Modules built as their own static libraries should use the CMake helpers below so their self-registrations survive the linker (see Modules and anchors).
| Lifetime | Instances | Built |
|---|---|---|
Singleton |
one, shared with the root | eagerly, when the container is created |
LazySingleton |
one, shared with the root | on first resolve |
Scoped |
one per scope | on first resolve within that scope |
Transient |
a fresh instance every resolve | on every resolve |
DI_DESCRIBE(Cache) { DI_SERVICE(ICache); DI_LIFETIME(Singleton); DI_MODULE(App); };
DI_DESCRIBE(Session) { DI_SERVICE(ISession); DI_LIFETIME(Scoped); DI_MODULE(App); };
DI_DESCRIBE(Command) { DI_SERVICE(ICommand); DI_LIFETIME(Transient); DI_MODULE(App); };DI_DEPENDS lists one wrapper per constructor parameter, in order:
| Wrapper | Constructor parameter | Meaning |
|---|---|---|
Di::Service<T> |
std::shared_ptr<T> |
required dependency |
Di::Keyed<T, DiKey("k")> |
std::shared_ptr<T> |
required, resolved by key |
Di::All<T> |
std::vector<std::shared_ptr<T>> |
every registered / exposed T |
Di::Optional<T> |
std::shared_ptr<T> (may be null) |
resolved if present, else null |
Di::Factory<T> |
std::function<std::shared_ptr<T>()> |
on-demand creator |
Di::Lazy<T> |
Di::DiLazy<T> |
deferred first-Get(); also breaks cycles |
class Report final : public IReport
{
public:
Report(std::shared_ptr<IClock> clock,
std::vector<std::shared_ptr<ISection>> sections,
Di::DiLazy<IRenderer> renderer);
};
DI_DESCRIBE(Report)
{
DI_SERVICE(IReport);
DI_DEPENDS(Di::Service<IClock>, Di::All<ISection>, Di::Lazy<IRenderer>);
DI_MODULE(App);
};Several implementations of one interface can coexist, told apart by key:
DI_DESCRIBE(ConsoleLogger) { DI_SERVICE(ILogger); DI_KEY("Console"); DI_LIFETIME(Singleton); DI_MODULE(App); };
DI_DESCRIBE(StreamLogger) { DI_SERVICE(ILogger); DI_KEY("Stream"); DI_LIFETIME(Singleton); DI_MODULE(App); };auto stream = provider.ResolveService<ILogger>("Stream"); // -> StreamLogger
auto all = provider.ResolveServices<ILogger>(); // -> both, keys ignoredDI_EXPOSE lets a type answer for an extra base interface too, so Di::All<> picks it up even when its primary DI_SERVICE is something more specific:
DI_DESCRIBE(FileLogger) { DI_SERVICE(IFileLogger); DI_EXPOSE(ILogger); DI_LIFETIME(Singleton); DI_MODULE(App); };
// A consumer with DI_DEPENDS(Di::All<ILogger>) receives FileLogger among all ILoggers.This is the same pattern the original C# version used for startup hooks, validators, and message handlers: register many implementations of one interface (keyed or not), then pull the whole set with Di::All<T> or ResolveServices<T>() — while Di::Service<T> / Di::Keyed<T, ...> still bind one specific registration, so the same implementations stay individually addressable by key when you need just one.
struct IInitializable
{
virtual ~IInitializable() = default;
virtual void Initialize() = 0;
};
DI_DESCRIBE(DatabaseWarmup) { DI_SERVICE(IInitializable); DI_KEY("Database"); DI_LIFETIME(Singleton); DI_MODULE(App); };
DI_DESCRIBE(CacheWarmup) { DI_SERVICE(IInitializable); DI_KEY("Cache"); DI_LIFETIME(Singleton); DI_MODULE(App); };
for (const auto& initializable : container.Provider().ResolveServices<IInitializable>())
initializable->Initialize();
auto cacheWarmup = container.Provider().ResolveService<IInitializable>("Cache"); // still addressable by keyA decorator wraps a resolved service in another implementation of the same interface. The decorator's first constructor parameter is the wrapped service; DI_DEPENDS lists anything else it needs:
class LoggingController final : public IController
{
public:
LoggingController(std::shared_ptr<IController> inner, std::shared_ptr<ILogger> logger);
};
DI_DECORATE(LoggingController)
{
DI_SERVICE(IController); // the interface being decorated
DI_DEPENDS(Di::Service<ILogger>); // extra dependencies, after the wrapped service
DI_DECORATOR(App);
};Decorators apply in registration order — last registered is outermost. In a manual composition root they can also be added with registry.Decorate<LoggingController>(). A decorated service shouldn't also use DI_EXPOSE.
A scope is a child provider with its own lifetime for Scoped services. Singletons stay shared with the root; transients are still fresh on every resolve.
{
auto scope = container.CreateScope();
auto unitOfWork = scope->ResolveService<IUnitOfWork>(); // one per scope
} // scope ends hereWhen a scope (or the container itself, for singletons) is destroyed, every created service implementing Di::Services::Abstraction::IDiDisposable has Dispose() called in reverse creation order:
class UnitOfWork final : public IUnitOfWork, public Di::Services::Abstraction::IDiDisposable
{
public:
void Dispose() override { m_Transaction.Rollback(); }
};
DI_DESCRIBE(UnitOfWork) { DI_SERVICE(IUnitOfWork); DI_LIFETIME(Scoped); DI_MODULE(App); };Self-registration lives in static initializers. In a static library, the linker drops object files nothing references — taking their registrations with them. Group a module's descriptors in one registration translation unit that anchors itself, and pull it in from the composition root:
// App/AppModule.cpp — the module's registration translation unit
#include "Di/Registry/DiModuleAnchor.hpp"
DI_MODULE_ANCHOR(App)
DI_DESCRIBE(Greeter) { /* ... */ DI_MODULE(App); };
DI_DESCRIBE(Clock) { /* ... */ DI_MODULE(App); };// main.cpp — composition root
DI_USE_MODULE(App)DI_USE_MODULE(App) references the module's anchor symbol through a dynamic initializer, forcing the linker to keep the whole translation unit. When each module is its own static library, wire them with the CMake helpers so retention is guaranteed:
di_add_module(App SOURCES App/AppModule.cpp) # requires exactly one *Anchor.cpp
di_module_link(my_app App) # honours the per-module whole-archive fallbackFor types you can't annotate, register them into a DiRegistry and build a container from it:
Di::Registry::DiRegistry registry;
registry.AddInstance<IClock>(std::make_shared<SystemClock>()); // a pre-built instance
registry.AddType<Greeter, IGreeter, Di::Service<IClock>>(); // a type with explicit deps
registry.AddFactory<IConnection>(EDiServiceLifetime::Singleton,
[](Di::Services::Abstraction::IDiServiceProvider&) { return OpenConnection(); });
registry.Decorate<LoggingController>();
Di::Container::DiContainer container(registry);The same DiRegistry API backs the attribute macros, so annotated and manual registrations mix freely.
Building a DiContainer validates the whole graph before constructing anything, and throws std::runtime_error with a precise message if it finds:
- a required dependency nothing provides (this covers lazy/scoped/transient services that eager singleton construction would never reach on its own);
- a dependency cycle — edges through
Di::Factory<T>andDi::Lazy<T>are treated as cycle-breaking, since they defer the resolve, so a graph that only closes a loop through one of those validates clean, matching the runtime behavior exactly.
This runs automatically whether the container is built from auto-discovery (DiContainer container;) or an explicit registry (DiContainer container(registry);), and constructs no instances, so it's cheap.
Two more guarantees hold independent of validation: cycle detection is always on at runtime (an A → B → A chain throws instead of overflowing the stack), and a singleton depending on a scoped service is rejected as a captive dependency (a singleton would otherwise outlive the per-scope instance it captured).
You can also validate a registry on its own:
Di::Registry::ValidateOrThrow(Di::Registry::DiRegistry::Process());Registration and resolution are split into three phases. DI_DESCRIBE specializes a DiDescriptor<T> trait, and a static registrar records a DiRegistrationRow (service type, key, lifetime, constructor, bindings, dependency list) — that's the describe phase. A reader then sorts rows by order, applies decorators, and builds an IDiServiceScheme per registration — compile. The service provider owns instances per lifetime, dispatches by scheme, and hands back correctly-typed shared_ptrs — resolve.
The C# original leans on reflection to walk constructors and read attributes at runtime; there's no equivalent at compile time in C++, so this port replaces it with dependency wrappers folded over a variadic pack (Di::Service<T>, Di::Keyed<T, ...>, and so on each expose the same static interface) to synthesize the constructor call, plus explicit template specialization instead of attribute reflection to attach a descriptor to a type. The core stays type-erased (shared_ptr<void> + std::function) so the machinery itself doesn't need to know concrete types, with typed casts only at the edges where a caller actually asks for T. The source under src/Di is organized to match these three phases.
cmake --preset ninja-debug
cmake --build --preset ninja-debug
ctest --preset ninja-debugPresets for ninja-release and Visual Studio (msvc) are provided too. Tests use Catch2, fetched automatically. Build without tests with -DDI_BUILD_TESTS=OFF. -DDI_WARNINGS_AS_ERRORS=ON turns the strict warning set (/W4 /permissive-, or -Wall -Wextra -Wpedantic) into errors.
- A C++20 compiler — MSVC 2022, Clang, or GCC
- CMake ≥ 3.24