Dependency Injection Container in C++: Fruit Library for Dependency Injection

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

Dependency Injection Container for C++: Overview of the Fruit Library



In modern C++ development, managing dependencies between classes becomes critically important for creating flexible, testable, and maintainable applications. The Dependency Injection (DI) pattern allows object creation to be moved outside of classes, while a DI container automates this process. One of the best libraries for implementing dependency injection in C++ is Fruit — a lightweight, fast, and type-safe container that works at compile time.



What is a Dependency Injection Container?

A Dependency Injection container is a tool that takes responsibility for creating and linking objects (dependencies). Instead of a class creating its own dependencies via new or factories, the container "injects" them from the outside. This implements the Inversion of Control (IoC) principle and makes the code loosely coupled.



Why Fruit?

The Fruit library (https://github.com/google/fruit) was developed at Google and differs from alternatives (e.g., Boost.DI) with the following features:

  • Compile-time checking: all bindings are checked at compile time, eliminating runtime errors related to missing dependencies.
  • Minimal overhead: Fruit does not use RTTI or virtual calls, generating efficient code.
  • Simplicity: only requires C++14 and header files — no macros or code generation.
  • Component support: you can assemble a dependency graph from reusable modules.


Installation

Fruit is distributed as a header-only library (only fruit.h is required). To install via vcpkg:

vcpkg install fruit

Or clone the repository and include the file include/fruit/fruit.h.



Core Concepts

Fruit uses three key elements:

  • Injector — the container that creates objects.
  • Component — a module that registers types and their factories.
  • Annotations — for distinguishing different implementations of the same interface.


Usage Example

Consider a classic example: a service depends on a repository. Let's create an interface and two implementations:



// interfaces.hpp#include <fruit/fruit.h>#include <string>

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

class IUserRepository {public: virtual std::string getUser(int id) = 0; virtual ~IUserRepository() = default;};


Implementations:

// implementations.cpp#include "interfaces.hpp"#include <iostream>

class ConsoleLogger : public ILogger {public: INJECT(ConsoleLogger()) = default; // Fruit annotation for constructor void log(const std::string& msg) override { std::cout << "[LOG] " << msg << std::endl; }};

class SqlRepository : public IUserRepository {public: INJECT(SqlRepository()) = default; std::string getUser(int id) override { return "User_" + std::to_string(id); }};


A service using both dependencies:

// user_service.cpp#include "interfaces.hpp"

class UserService {private: ILogger* logger; IUserRepository* repo;public: INJECT(UserService(ILogger* logger, IUserRepository* repo)) : logger(logger), repo(repo) {} void processUser(int id) { auto name = repo->getUser(id); logger->log("Processing user: " + name); }};


Now let's assemble the component

Recommendations