Dependency Injection in C++

In this post, I will switch gears from functional C++ to object oriented C++ and talk about dependency injection.

Let’s start with a simple example: take a Car class with a Drive() method. Let’s say this class contains a V8Engine attribute with Start() and Stop() methods. An initial implementation might look like this:

V8Engine.h (publicly visible):

#pragma once

class V8Engine
{
public:
    void Start();
    void Stop();
};

V8Engine.cpp:

#include "V8Engine.h"

V8Engine::Start()
{
    // start the engine
}

V8Engine::Stop()
{
    // stop the engine
}

Car.h (publicly visible):

#pragma once
#include "V8Engine.h"

class Car
{
public:
    void Drive();

private:
    V8Engine m_engine;
};

Car.cpp:

#include "Car.h"

void Car::Drive()
{
    m_engine.Start();
    // drive
    m_engine.Stop();
}

Dependency Injection with Interfaces

In the above example, Car is tightly coupled to V8Engine, meaning we can’t create a car without a concrete engine implementation. If we want the ability to swap various engines or use a mock engine during testing, we could reverse the dependency by creating an IEngine interface and decoupling Car from the concrete V8Engine implementation. This way, we only expose an IEngine interface and a factory function. Car can work against that:

IEngine.h (publicly visible):

#pragma once

struct IEngine
{
    virtual void Start() = 0;
    virtual void Stop() = 0;
    virtual ~IEngine() = default;
};

std::unique_ptr<IEngine> MakeV8Engine();

V8Engine.cpp:

#include "IEngine.h"

class V8Engine : public IEngine
{
    void Start() override { /* start the engine */ }
    void Stop() override { /* stop the engine */ }
};

std::unique_ptr<IEngine> MakeV8Engine()
{
    return std::make_unique<V8Engine>();
}

Car.h (publicly visible):

#pragma once
#include "IEngine.h"

class Car
{
public:
    Car(std::unique_ptr<IEngine>&& engine)
        : m_engine(std::move(engine))
    {
    }

    void Drive();
private:
    std::unique_ptr<IEngine> m_engine;
};

Car.cpp:

#include "Car.h"

void Car::Drive()
{
    m_engine->Start();
    // drive
    m_engine->End();
}

Notes

A note on headers

Headers simply get textually included in each compilation unit by the #include directive. It is not mandatory to provide a header file for each class declaration. If a class can be scoped to a single source file, then it doesn’t need a header declaration (for example the V8Engine class above does not need a V8Engine.h header corresponding to the V8Engine.cpp). It is also a good idea to have public headers and internal headers: public headers contain the public API surface and can be included by other parts of the system, while internal headers are only used within the component and should not be included by external code.

Default should be the least visible: try to keep everything inside the cpp file (like V8Engine.cpp). If that is not enough, an internal header might do. A declartion should be pulled into a public header only when external components need to reference it.

A note on interfaces

It’s a good idea to declare a default virtual destructor: if a deriving type has a destructor, it won’t get called if we store an upcast pointer to the interface unless the interface declares a virtual destructor. Note a destructor does not to be expicitly defined - compiler might generate a default one.

MSVC compiler provides a __declspec(novtable) [1] custom attribute which tells the compiler not to generate a vtable for pure abstract classes. This reduces code size. Below is the IEngine declaration with this attribute:

struct __declspec(novtable) IEngine
{
    virtual void Start() = 0;
    virtual void Stop() = 0;
    virtual ~IEngine() = default;
};

I won’t include it in the code samples in this post, but it’s worth keeping in mind when working with MSVC.

A note on factory functions

When working with interfaces as opposed to concrete types, we use factory functions to get object instances. Below is a possible naming convention, taking object ownership into account:

std::unique_ptr<IFoo> MakeFoo();
IFoo& UseFoo();
std::shared_ptr<IFoo> GetFoo();

The first function, MakeFoo, returns a unique pointer, passing ownership to the caller. Like in the example above, the unqiue_ptr can be moved into the object, which ends up owning it. Use a Make when each call creates a new instance.

The second function implies there already exists an IFoo object which is owned by someone else, with the guarantee that it will outlive the caller. In that case, there is no need for pointers and we can simply return a reference to the object. This can be used, for example, for singletons. Below is an example of a singleton Engine:

IEngine& UseEngine()
{
    static auto instance = std::make_unique<Engine>();
    return *instance;
}

The third function, GetFoo, implies shared ownership - we get an object that other objects might hold a reference to, but we don’t have the lifetime guarantee a singleton would give us, so we need to use a shared pointer to make sure the object is kept alive long enough.

Mocking

Since Car now works with an IEngine interface, in test code we can mock the engine:

Test.cpp:

#include "Car.h"

class MockEngine : public IEngine
{
    void Start() override { /* mock logic */ }
    void Stop() override { /* mock logic */ }
};

void Test()
{
    Car car(std::make_unique<MockEngine>());

    // Test Car without a real Engine
}

We can also expose Car as a simple interface, hiding its implementation details, in which case we would end up with the following:

ICar.h (publicly visible):

#pragma once
#include "IEngine.h"

struct ICar
{
    virtual void Drive() = 0;
    virtual ~ICar() = default;
};

std::unique_ptr<ICar> MakeCar(std::unique_ptr<IEngine> &&engine);

Car.cpp:

#include "ICar.h"

class Car : public ICar
{
public:
    Car(std::unique_ptr<IEngine>&& engine)
         : m_engine(std::move(engine))
    {
    }

    void Drive() override
    {
         m_engine->Start();
         // drive
         m_engine->Stop();
    }

private:
    std::unique_ptr<IEngine> m_engine;
};

std::unique_ptr<ICar> MakeCar(std::unique_ptr<IEngine>&& engine)
{
    return std::make_unique<Car>(std::move(engine));
}

Test would become:

#include "ICar.h"

class MockEngine : public IEngine
{
    void Start() override { /* mock logic */ }
    void Stop() override { /* mock logic */ }
};

void Test()
{
    auto car = MakeCar(std::make_unique<MockEngine>());

    // Test ICar without a real Engine
}

Note this allows the caller to pass in any IEngine. We provide an out-of-the-box V8Engine but other engines can be injected when Car gets constructed. The headers IEngine.h and ICar.h are public per our above defintion.

In general, it’s great if we can get the rest of the component code and unit tests to work against the interface. Sometimes though we might need to know more about the actual implementation inside our component, even if externally we only expose an interface. In that case, we can add an internal Car.h header:

Car.h (internal):

#pragma once
#include "ICar.h"

class Car : public ICar
{
public:
    Car(std::unique_ptr<IEngine>&& engine)
         : m_engine(std::move(engine))
    {
    }

    void Drive() override;

private:
    std::unique_ptr<IEngine> m_engine;
};

Car.cpp becomes:

#include "Car.h"

void Car::Drive()
{
    m_engine.Start();
    // drive
    m_engine.Stop();
}

std::unique_ptr<ICar> MakeCar(std::unique_ptr<IEngine>&& engine)
{
    return std::make_unique<Car>(std::move(engine));
}

Now we can include the internal header, and, while not necessarily recommended, we can cast ICar to Car inside the component:

auto icar = MakeCar(MakeV8Engine());
auto& car = static_cast<Car&>(*car);

Another trick if needing access to internals (again, not something necessarily recommended), is to make the unit test class testing Car a friend of the Car class, in which case it can access its private members.

In summary, with this approach we are able to:

  • Hide implementation details in the .cpp files
  • Work against abstract interfaces
  • Inject dependencies during object construction

Dependecy Injection with Templates

An alternative to the above is to use templates. In this case, we would have to provide the implementation inside the header file, as code needs to be available when templates get instantiated:

V8Engine.h (publicly visible):

#pragma once

class V8Engine
{
public:
    void Start();
    void Stop();
};

V8Engine.cpp:

#include "V8Engine.h"

V8Engine::Start()
{
    // start the engine
}

V8Engine::Stop()
{
    // stop the engine
}

Car.h (publicly visible):

#pragma once

template <typename TEngine>
class Car
{
public:
    void Drive()
    {
        m_engine.Start();
        // drive
        m_engine.Stop();
    }

private:
    TEngine m_engine;
};

Note Car is implemented in the header and V8Engine is also a publicly visible header. Now we can create an instance of Car like this:

#include "V8Engine.h"
#include "Car.h"

...

Car<V8Engine> car;

Mocking the engine in test code would look like this:

#include "Car.h"

class MockEngine
{
    void Start() { /* mock logic */ }
    void Stop() { /* mock logic */ }
};

void Test()
{
    Car<MockEngine> car;

    // Test Car without a real Engine
}

With this approach we are able to:

  • Inject dependencies during template instantiation
  • No need for virtual calls (note TEngine is not an interface, so calls can be resolved at compile-time)
  • Car<T> can be default-constructed

A drawback here is we expose the implementation details of Car inside the header file and we have to make this publicly visible.

Hybrid Approach

We can use a hybrid approach if we don’t need an externally injected Engine. Say our component provides a V8Engine, a V6Engine, and we have a MockEngine used during testing. We have the same componentization requirements but don’t need to expose all the details to consumers. In that case we could have something like this:

ICar.h (publicly visible):

#pragma once

struct ICar
{
    virtual void Drive() = 0;
    virtual ~ICar() = default;
};

std::unique_ptr<ICar> MakeV8Car();
std::unique_ptr<ICar> MakeV6Car();

Car.h (internal):

#pragma once
#include "ICar.h"

template <typename TEngine>
class Car : public ICar
{
public:
    void Drive() override
    {
        m_engine.Start();
        // drive
        m_engine.Stop();
    }

private:
    TEngine m_engine;
};

Car.cpp:

#include "Car.h"
#include "V8Engine.h"
#include "V6Engine.h"

std::unique_ptr<ICar> MakeV8Car()
{
    return std::make_unique<Car<V8Engine>>();
}

std::unique_ptr<ICar> MakeV6Car();
{
    return std::make_unique<Car<V6Engine>>();
}

Test would remain the same as in the example above, where we worked against a Car type (not an ICar) which we instantiate with a MockEngine.

With this approach:

  • Our external API is an interface
  • Internally we still inject the dependency using a template

With this approach, we do have an interface and virtual calls for Car but not for TEngine types. One drawback with this approach is that consumers cannot inject their own Engine type: we can only create cars with engines that are known within our component.

Summary

We decoupled Car from V8Engine and looked at three ways of injecting the dependency:

  • Using interfaces, where dependency is injected at runtime during object creation
  • Using templates, where dependency is injected at compile-time during template instantiation
  • A hybrid approach which uses templates internally but exposes only interfaces publicly

Each of these approaches has pros and cons, the tradeoffs mostly being around encapsulation (how much of the component code we expose publicly), runtime (templates are instantiated at compile-time so no virtual calls etc.), type constraints (with templates we don’t require engines to implement a particular IEngine interface), and flexibility (with the hybrid approach we can’t inject an external engine, we can only use what the component has available internally).


[1]For more details on novtable, see MSDN.