Smart pointers and the pointer to implementation idiom
A post I wrote back in 2023 When an empty destructor is required resulted in feedback that I'd like to address in today's post.
In the 2023 post, I briefly mentioned PImpl idiom. I did not intend to make it the theme of the post. However, I got various questions about PImpl and smart pointers.
The goal of PImpl is to hide implementation details from clients. Since you can declare a pointer of an unknown class, you can shift the entire implementation of such an anonymous class into a .cpp
file. That way, no client can see any details. Another benefit is that changes to that .cpp
file result in only minor recompiles. Maintaining the same in a header file would cause all .cpp
files, including this header, to recompile. At least the speed-up part is since C++20's modules are no longer necessary. And as long as you don't want to hide classified implementation in the .cpp
file modules, it also gives you the ability to mark constructs as private.
But let's assume you're still going for PImpl. Here is the code from the previous post:
1 2 3 4 5 6 7 |
|
Delaying the destructor of Apple
as I explained in When an empty destructor is required only helps you until you're going to create an Apple
object. Suppose you have the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
For PImpl, this should be a perfectly valid code, at least to compile. Once you link, the definition of Orange
must be present in one of the object files. Yet, the code as presented doesn't compile! You end up with an error message like the one below.
1 2 3 |
|
You might remember from Understanding the inner workings of C++ smart pointers - The unique_ptr, that a unique_ptr
comes with a second template parameter the deleter. Due to the implementation, this deleter gets instantiated once a unique_ptr
gets instantiated. For the example above, this happens once you create an object of type Apple
because this object then instantiates the contained unique_ptr
.
shared_ptr
to the rescue
Since you seem to remember my previous post Understanding the inner workings of C++ smart pointers - The unique_ptr, you hopefully also read the second part Understanding the inner workings of C++ smart pointers - The shared_ptr. If not, now might be a good moment.
Assuming you're up to speed, one difference between the unique_ptr
and shared_ptr
is that the latter uses type erasure when storing the deleter. The reason isn't to support PImpl but to make the mechanics for the reference counting work. Anyhow, for the current code, a shared_ptr
works like you expect when implementing PImpl.
1 2 3 4 5 6 7 8 9 10 |
|
The code above doesn't even need to declare an out-of-line destructor. That is another advantage here.
More to come
But can't we get unique_ptr
to work? A shared_ptr
comes with a lot of overhead that brings no value to our example. Well, that's my next post topic. Stay tuned!
Andreas