Jonathan Boccara's blog

How to implement the pimpl idiom by using unique_ptr

Published September 22, 2017 - 16 Comments

The pimpl, standing for “pointer to implementation” is a widespread technique to cut compilation dependencies.

There are a lot of resources about how to implement it correctly in C++, and in particular a whole section in Herb Sutter’s Exceptional C++ (items 26 to 30) that gets into great details.

There is one thing that I’ve found a bit less documented though: how to implement the pimpl idiom with a smart pointer (although excellent and still relevant today, Exceptional C++ was published before smart pointers came into the standard).

Indeed, the pimpl idiom has an owning pointer in charge of managing a memory resource, so it sounds only logical to use a smart pointer, such as std::unique_ptr for example.

EDIT: several people had the kindness to point out that while the book has not been updated, Herb Sutter has an updated version of the topic on its Guru of the week, items 100 and 101 in particular.

This post is part of the series Smart Developers Use Smart Pointers:

The pimpl

pimpl unique_ptrJust to have a common basis for discussion, I’m quicky going to go over the pimpl principle by putting together an example that uses it.

Say we have a class reprenseting a fridge (yeah why not?), that works with an engine that it contains. Here is the header of this class:

#include "Engine.h"

class Fridge
{
public:
   void coolDown();
private:
   Engine engine_;
};

(the contents of the Engine class are not relevant here).

And here is its implementation file:

#include "Fridge.h"

void Fridge::coolDown()
{
   /* ... */
}

Now there is an issue with this design (that could be serious or not, depending on how many clients Fridge has). Since Fridge.h #includes Engine.h, any client of the Fridge class will indirectly #include the Engine class. So when the Engine class is modified, all the clients of Fridge have to recompile, even if they don’t use Engine directly.

The pimpl idiom aims at solving this issue by adding a level of indirection, FridgeImpl, that takes on the Engine.

The header file becomes:

class Fridge
{
public:
   Fridge();
   ~Fridge();

   void coolDown();
private:
   class FridgeImpl;
   FridgeImpl* impl_;
};

Note that it no longer #include Engine.h.

And the implementation file becomes:

#include "Engine.h"
#include "Fridge.h"

class Fridge::FridgeImpl
{
public:
   void coolDown()
   {
      /* ... */
   }
private:
   Engine engine_;
};

Fridge::Fridge() : impl_(new FridgeImpl) {}

Fridge::~Fridge()
{
   delete impl_;
}

void Fridge::coolDown()
{
   impl_->coolDown();
}

The class now delegates its functionalities and members to FridgeImpl, and Fridge only has to forward the calls and manage the life cycle of the impl_ pointer.

What makes it work is that pointers only need a forward declaration to compile. For this reason, the header file of the Fridge class doesn’t need to see the full definition of FridgeImpl, and therefore neither do Fridge‘s clients.

Using std::unique_ptr to manage the life cycle

Today it’s a bit unsettling to leave a raw pointer managing its own resource in C++. A natural thing to do would be to replace it with an std::unique_ptr (or with another smart pointer). This way the Fridge destructor no longer needs to do anything, and we can leave the compiler automatically generate it for us.

The header becomes:

#include <memory>

class Fridge
{
public:
   Fridge();
   void coolDown();
private:
   class FridgeImpl;
   std::unique_ptr<FridgeImpl> impl_;
};

And the implementation file becomes:

#include "Engine.h"
#include "Fridge.h"

class FridgeImpl
{
public:
   void coolDown()
   {
      /* ... */
   }
private:
   Engine engine_;
};

Fridge::Fridge() : impl_(new FridgeImpl) {}

Right? Let’s build the program…

Oops, we get the following compilation errors!

use of undefined type 'FridgeImpl'
can't delete an incomplete type

Can you see what’s going on here?

Destructor visibility

There is a rule in C++ that says that deleting a pointer leads to undefined behaviour if:

  • this pointer has type void*, or
  • the type pointed to is incomplete, that is to say is only forward declared, like FridgeImpl in our header file.

std::unique_ptr happens to check in its destructor if the definition of the type is visible before calling delete. So it refuses to compile and to call delete if the type is only forward declared.

In fact, std::unique_ptr is not the only component to provide this check: Boost also proposes the checked_delete function and its siblings to make sure that a call to delete is well-formed.

Since we removed the declaration of the destructor in the Fridge class, the compiler took over and defined it for us. But compiler-generated methods are declared inline, so they are implemented in the header file directly. And there, the type of FridgeImpl is incomplete. Hence the error.

The fix would then be to declare the destructor and thus prevent the compiler from doing it for us. So the header file becomes:

#include <memory>

class Fridge
{
public:
   Fridge();
   ~Fridge();
   void coolDown();
private:
   class FridgeImpl;
   std::unique_ptr<FridgeImpl> impl_;
};

And we can still use the default implentation for the destructor that the compiler would have generated. But we need to put it in the implementation file, after the definition of FridgeImpl:

#include "Engine.h"
#include "Fridge.h"

class FridgeImpl
{
public:
   void coolDown()
   {
      /* ... */
   }
private:
   Engine engine_;
};

Fridge::Fridge() : impl_(new FridgeImpl) {}

Fridge::~Fridge() = default;

And that’s it! It compiles, run and works. It wasn’t rocket science but in my opinion still good to know, to avoid puzzling over a problem that has a perfectly rational explanation.

Of course, there are plenty other important aspects to consider when implementing a pimpl in C++. For this I can only advise you to have a look at the dedicated section in Herb Sutter’s Exceptional C++.

Related articles:

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin

Comments are closed