pImpl C++ tutorial
14 May 2021
The Pointer to Implementation (pImpl) idiom in C++ is one technique that allows you to hide implementation
details from an interface.
Some practical benefits of pImpl are:
- Optimize compilation time
Changes in the implementation do not require components that depend on the interface to be recompiled. This is very important when distributing precompiled libraries since your users’ software will be binary compatible with your different deliveries regardless of the changes you do as long as the public interface is not modified. - Hide implementation completely when distributing a precompiled library.
If you want to share your library without giving access to the source code, you typically distribute one or more header files that include the various declarations used during compilation along with a (shared) library (e.g. a.soor.dllfile). The precompiled library includes the definitions/implementations of the contents of the header file(s) and is utilized during linking.
pImplallows you to not share dependencies and private member declarations which would have otherwise been necessary to be included in the header file. - Switch class implementations, transparently to the user of the class.
Determine which class to use during linking.
This tutorial can also be found on YouTube.
Code to refactor
Let’s assume a gyroscope that uses the I2C bus to transmit data and here’s its C++ representation:
// Gyroscope.h
#include "I2cCommunicationBus.h"
class Gyroscope
{
public:
Gyroscope(int temperature);
int getOrientation();
private:
const int mTemperature;
I2cCommunicationBus i2c;
};
// Gyroscope.cpp
Gyroscope::Gyroscope(int temperature)
: mTemperature{temperature}
{
}
int Gyroscope::getOrientation()
{
const auto registerValue = i2c.read(0xFF);
const auto adjustedOrientation = registerValue + mTemperature / 2;
return adjustedOrientation % 360;
}
We face the following issues:
- Any changes to the
Gyroscopeclass implementation (or its dependencies) will require recompilation of software that depends/uses theGyroscopeclass. - Distributing the
Gyroscopeas a shared library will require you to not only distributeGyroscope.hbut alsoI2cCommunicationBus.hand so on. - If a Gyroscope variant is introduced and this should be transparent to the user, it may not be
trivial to switch implementations during linking when details are leaked through the header file.
For example, if the user’s code needs to run on a different hardware platform where
SPIis used instead ofI2C, the dependency to I2C is still visible through its inclusion inGyroscope.h.
pImpl
Gyroscope interface (public)
One way to solve the problems outlined above is by applying the pImpl idiom. The key concept is to
hide all implementation details and dependencies into .cpp files by forward declaring an
“implementation” class and maintain a pointer to it as a member variable.
// Gyroscope.h
class Gyroscope
{
public:
Gyroscope(int temperature);
~Gyroscope();
int getOrientation();
private:
class GyroscopeImpl; // Very secret or platform-specific
std::unique_ptr<GyroscopeImpl> mGyroscope;
};
Gyroscope.h can be shared with our users and includes the bare minimum information they must know to
integrate our class into their code.
As you can see we have forward-declared the GyroscopeImpl class, which is to be defined elsewhere, along with
a unique pointer to a GyroscopeImpl instance.
I2C implementation (private)
// I2cGyroscopeImpl.cpp
#include "Gyroscope.h"
#include "I2cCommunicationBus.h"
class Gyroscope::GyroscopeImpl
{
public:
GyroscopeImpl(int temperature)
: mTemperature{temperature}
{
}
int getOrientation()
{
const auto registerValue = i2c.read(0xFF);
const auto adjustedOrientation = registerValue + mTemperature / 2;
return adjustedOrientation % 360;
}
private:
const int mTemperature;
I2cCommunicationBus i2c;
};
In the I2cGyroscopeImpl.cpp file, which will not be shared with our users and its contents will eventually
reside in a shared library, we have the definition of the GyroscopeImpl class that we forward-declared
in Gyroscope.h.
Note that the dependency to I2cCommunicationBus is no longer exposed via the Gyroscope.h we share with
the users.
Then, we define the Gyroscope class that merely relays information to and from the GyroscopeImpl.
Gyroscope::Gyroscope(int temperature)
: mGyroscope{std::make_unique<GyroscopeImpl>(temperature)}
{
}
Gyroscope::~Gyroscope()
{
// Defined this in the .cpp file(s) or you will get incomplete type errors
}
int Gyroscope::getOrientation()
{
return mGyroscope->getOrientation();
}
Here you should be aware that the Gyroscope destructor cannot be default constructed or defined in the
Gyroscope.h header file, since at the time it is not known to the compiler how to destruct the member variable
(that is a unique_ptr). Keep in mind that in Gyroscope.h, we have only forward-declared the GyroscopeImpl class.
Nothing else is known about it and therefore the Gyroscope destructor does not know how to destruct it.
This is why we would have to define the Gyroscope destructor in our implementation file and after it is known
how to destruct the GyroscopeImpl class, i.e. after the GyroscopeImpl class’ destructor has been (default)
constructed.
SPI implementation (private & testable)
Declaring and defining a class entirely inside an implementation file does not mean you should throw design best
practices out of the window. For example, in I2cGyroscopeImpl.cpp our business logic is tightly coupled to the
I2cCommunicationBus class which is concrete.
We can still inject dependencies by breaking the actual implementation in separate files,
e.g. SpiGyroscope.h and SpiGyroscope.cpp.
// SpiGyroscope.h
#include "CommunicationBus.h"
class SpiGyroscope
{
public:
SpiGyroscope(CommunicationBus& communicationBus, int temperature);
int getAngularDisplacement();
private:
CommunicationBus& mCommunicationBus; // This is a pure abstract interface!
const int mTemperature;
};
This allows the GyroscopeImpl class to be considered as the “integration scope” and SpiGyroscope to contain
the core business logic that should be unit tested.
// SpiGyroscopeImpl.cpp
#include "Gyroscope.h"
#include "SpiCommunicationBus.h"
#include "SpiGyroscope.h"
class Gyroscope::GyroscopeImpl
{
public:
GyroscopeImpl(int temperature)
: mGyroscope{mSpi, temperature}
{
}
int getOrientation()
{
return mGyroscope.getAngularDisplacement();
}
private:
SpiCommunicationBus mSpi;
SpiGyroscope mGyroscope;
};

