Dependency Injection in C++ in Practice: Examples and Patterns

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why is Dependency Injection Important in C++?



In modern C++ development, maintaining loose coupling between classes is key to code flexibility and testability. Dependency Injection (DI) is a pattern where an object receives its dependencies from the outside, rather than creating them itself. This directly implements the Dependency Inversion Principle from SOLID.



Without DI, classes are often tightly coupled to specific implementations:



// Bad: the class creates the dependency itselfclass ReportGenerator {    Database db_; // Tight coupling to a specific databasepublic:    ReportGenerator() : db_("localhost") {}    void generate() { db_.query("SELECT ..."); }};


With DI, we gain flexibility, the ability to swap implementations (e.g., for unit tests), and simplify code maintenance. In this article, we will explore three practical ways to implement DI in C++: through the constructor, through interfaces, and using a simple container.



1. Constructor Injection



The most common and recommended approach. Dependencies are passed to the class constructor as parameters. In C++, it is convenient to use smart pointers (std::unique_ptr, std::shared_ptr) or references for this.



Example: Logger and Service



#include <iostream>#include <memory>#include <string>

// 1. Abstraction (interface)class ILogger {public: virtual ~ILogger() = default; virtual void log(const std::string& msg) = 0;};

// 2. Concrete implementationclass ConsoleLogger : public ILogger {public: void log(const std::string& msg) override { std::cout << "[LOG]: " << msg << std::endl; }};

// 3. Class that depends on ILoggerclass UserService { std::shared_ptr<ILogger> logger_; // Dependencypublic: // Constructor injection explicit UserService(std::shared_ptr<ILogger> logger) : logger_(std::move(logger)) {}

void createUser(const std::string& name) { // Business logic... logger_->log("User created: " + name); }};

int main() { auto logger = std::make_shared<ConsoleLogger>(); UserService service(logger); service.createUser("Alice"); return 0;}


Advantages:

  • The dependency is explicitly visible in the constructor signature.
  • The object is always in a valid state (after creation).
  • Easy to swap implementations for tests (e.g., MockLogger).


Disadvantage: with a large number of dependencies, the constructor can become cumbersome. In such cases, containers help (see section 3).



2. Injection through Interfaces and Polymorphism



This is an evolution of the previous approach. We define pure virtual interfaces for all dependencies. The client code works only with abstractions, and concrete implementations are provided at the object assembly stage (composition root).



Example: Repository and Business Logic



#include <vector>#include <memory>

// Repository interfaceclass IUserRepository {public: virtual ~IUserRepository() = default; virtual void add(const std::string& user) = 0; virtual std::vector<std::string> getAll() const = 0;};

// Database implementationclass DatabaseUserRepo : public IUserRepository {public: void add(const std::string& user) override { // SQL query } std::vector<std::string> getAll() const override { return {"Alice", "Bob"}; }};

// Business logic, unaware of the databaseclass UserManager { std::uniq

Blogs

Book Recommendations