Sunday 14 February 2016

The Pimpl idiom

Having a long-time C# background, I have spent some time understanding this C++ idiom.

You can implement this idiom for several reasons (I will mention them later on), but the one every article I have read mentions is to speed up compile time.

To fully understand this statement, you need to know how the C++ compiler works.

Every time you declare a variable in your program, the compiler will search the class declaration and definition because every type needs to be fully defined.

Before starting, please note that my IDE is Visual Studio 2015, which provides an almost-C++11/14/17 compliant compiler.

Let's start from a small example:
#include <iostream>
#include "MyTimer.h"

int main (int argc, char *argv[])
{
    MyTimer timer;
    std::cout << timer.GetMillisecsFromStart();

    return 0;
}


MyTimer implements a scoped timer functionality: it will start its internal counter upon construction and stop it upon destruction.

It also allows a caller to know the number of milliseconds that have passed from the timer's start.

When the compiler reads the first line of the function body, it will search the definition of the MyTimer type in the known compilation units.

Every part of the type, even the private one, needs to be defined because the compiler needs to know the exact size of the objects.
It will do the same every time a cpp file uses the MyTimer type (and include the MyTimer.h header.

In very large projects, this will increase compilation times.
Let's see now how the Pimpl idiom can speed up compile times.
Pimpl stands for pointer to implementation; this means moving your class's implementation to another class/struct and maintaining only a pointer to it.

The following code shows an example implementation of MyTimer:
MyTimer.h
#include <chrono> 

using namespace std::chrono;
class MyTimer
{
public:
    MyTimer();
    uint64_t GetMilliSecsFromStart();

private:
    time_point<steady_clock> Now();
    time_point<steady_clock> m_start;
}

MyTimer.cpp
#include "MyTimer.h"

MyTimer::MyTimer()
    : m_start(steady_clock::now())
{
}

uint64_t MyTimer::GetMilliSecsFromStart()
{
    return duration_cast<milliseconds>(Now() - m_start).count();
}

time_point<steady_clock> MyTimer::Now()
{
    return steady_clock::now();
}


Implementation

Let's try now to implement the Pimpl idiom:

1) Move all the private members to another class/struct

MyTimer.cpp
#include "MyTimer.h"

struct MyTimerImpl
{
    time_point<steady_clock> Now()
    {
        return steady_clock::now();
    }

    time_point<steady_clock> m_start;  
}

...


2) Declare a private smart pointer to the new implementation class/struct:

MyTimer.h
#include <memory> 

class MyTimer
{
public:
    MyTimer();
    uint64_t GetMilliSecsFromStart();

private:
    std::shared_ptr<MyTimerImpl> m_impl; // this should be a more appropriate
                                         // unique_ptr. Ignore it for the moment.
}


3) Construct the object containing the implementation:

MyTimer.cpp
...

Timer::Timer()
    : m_impl(std::make_shared<MyTimerImpl>())
{
}

...

4) Change the public methods in order to use the internal implementation:

MyTimer.cpp
...

uint64_t Timer::GetMilliSecsFromStart()
{
    return duration_cast<milliseconds>(m_impl->Now() - m_impl->m_start).count();
}

...



Here the resulting files after the implementation:

MyTimer.h
#include <memory>

class MyTimer
{
public:
    MyTimer();
    uint64_t GetMilliSecsFromStart();

private:
    struct MyTimerImpl;                  // Forward declaration
    std::shared_ptr<MyTimerImpl> m_impl; // This should be a more appropriate
                                         // unique_ptr. Ignore it for the moment.
};

MyTimer.cpp
#include "MyTimer.h"
#include <chrono>

using namespace std::chrono;
struct MyTimer::MyTimerImpl
{
    MyTimerImpl()
        : m_start(steady_clock::now())
    {
    }

    time_point<steady_clock> Now()
    {
        return steady_clock::now();
    }

    time_point<steady_clock> m_start;
};

MyTimer::MyTimer()
    : m_impl(std::make_shared<MyTimerImpl>())
{
}

uint64_t MyTimer::GetMilliSecsFromStart()
{
    return duration_cast<milliseconds>(m_impl->Now() - m_impl->m_start).count();
}


The compiler does not complain about this code because it has enough information on all the types. A (smart) pointer to an incomplete type is still valid because the size of a pointer is known.
Note I have used a std::shared_ptr on purpose to show you later in this article how using a more appropriate std::unique_ptr forces you to introduce a (minor) change in the code.
Regarding the new implementation, there are a few interesting points:

  • The header does not contain any reference to std::chrono;
  • The compiler is not forced to resolve the implementation dependencies every time a client code references the MyTimer class.
  • All the details of the timer been used are inside the MyTimerImpl struct; this allows us to change the implementation details easily; for example, replacing steady_clock with std::chrono::high_resolution_clock. More generally, platform-independent APIs can benefit from the PImpl idiom to hide the platform-dependent implementation details.
  • This fact also implies that the client code does not need to be recompiled upon MyTimerImpl change.
Now, let me elaborate on the type of smart pointer I used.

The "pointer to implementation" is a good use case for a unique_ptr; in fact, the ownership of this pointer belongs only to a MyTimer object, and the lifetime of the pointed object should be the same as a MyTimer object.

There is no harm in using a shared_ptr, but semantically unique_ptr is the correct type.

Let's change the code above to use a unique_ptr instead of a shared_ptr:

MyTimer.h
...

private:
    struct MyTimerImpl;                  // forward declaration
    std::unique_ptr<MyTimerImpl> m_impl; // Much better :)
};



MyTimer.cpp
...

MyTimer::MyTimer()
    : m_impl(std::make_unique<MyTimerImpl>())
{
}

...


Easy, right?

Unfortunately, the compiler will start complaining that MyTimerImpl is an incomplete type.
What happened?

The problem here is the default destructor of MyTimer the compiler is so kindly implicitly providing does not have enough information on MyTimerImpl: a side-effect of changing to unique_ptr, a non-copyable type (i.e. copy constructor and copy assignment operator are deleted).
For a profound explanation of this behaviour, please refer to Item 22 of the excellent Effective Modern C++ by Scott Meyers.

We can solve the issue by moving the implementation of the destructor to after the complete definition of MyTimerImpl:

MyTimer.h
...

public:
    MyTimer();
    ~MyTimer();

...


MyTimer.cpp
...

MyTimer::MyTimer()
    : m_impl(std::make_unique<MyTimerImpl>())
{
}

MyTimer::~MyTimer() = default;

...


The above tells the compiler to create the default destructor implementation at that precise point of the cpp file. Since it is after the definition of MyTimerImpl, the compiler has all the necessary information to create the body of the default destructor.



Conclusions

The PImpl idiom is another tool in the arsenal of a good C++ developer.
These are the main reasons to implement it:
  • Improve compilation time;
  • Hide class implementation;
  • Write code for different platforms;

By writing this article, I have improved my knowledge of the compiler behaviour, especially regarding incomplete types and the special default members.