Logo

Blog


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
A Header file
class Apple {
  std::unique_ptr<class Orange> mOrange{};

public:
  ~Apple();
};

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
A Header file
class Apple {
  std::unique_ptr<class Orange> mOrange{};

public:
  ~Apple();
};

void Test()
{
  B Some use after including the header
  Apple a{};
}

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
unique_ptr.h:97:16: error: invalid application of 'sizeof' to an incomplete type 'Orange'
   97 |         static_assert(sizeof(_Tp)>0,
      |                       ^~~~~~~~~~~

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
A Header file
class Apple {
  std::shared_ptr<class Orange> mOrange{};
};

void Test()
{
  B Some use after including the header
  Apple a{};
}

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