How to implement the pimpl idiom by using unique_ptr
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:
- Smart pointer basics
- unique_ptr, shared_ptr, weak_ptr, scoped_ptr, raw pointers: clearly stating your intentions by knowing your smart pointers
- Custom deleters and How to make them more expressive
- Changing deleters during the life of a unique_ptr
- How to implement the pimpl idiom by using unique_ptr
- How to make a polymorphic clone in modern C++
- How to Return a Smart Pointer AND Use Covariance (by Raoul Borges)
The pimpl
Just 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
#include
s 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:
- Smart pointer basics
- unique_ptr, shared_ptr, weak_ptr, scoped_ptr, raw pointers: clearly stating your intentions by knowing your smart pointers
- Custom deleters and How to make them more expressive
- Changing deleters during the life of a unique_ptr
- How to make a polymorphic clone in modern C++
- How to Return a Smart Pointer AND Use Covariance (by Raoul Borges)
Share this post!