Hello again!
In this video, we are going to look at the so-called "pImpl" idiom.
There are various ways you can implements the Handle-Body pattern: the pImpl idiom is a very
popular way of doing it.
The handle class, which has the interface, has a private member, which is a pointer to a body object,
which has the actual implementation.
So that is a "pointer to implementation" or "pImpl".
And pronouncing it as "pimple" is easier and slightly more amusing.
It's also known as a compiler firewall, because it introduces a rigid separation between implementation
code and client code.
So that reduces the amount of recompilation that is needed if the implementation gets changed.
In the Handle class,
there is a pointer to the Body.
We do not need the full definition of the Body class for this, just a forward declaration.
So the compiler knows that Body is a class.
So this means we do not need to include the Body header in this Handle
header.
The interface is just going to forward to the Body interface.
So when we call a member function here, this will call the corresponding member function on the Body.
The Handle class is implemented using RAII.
The constructor allocates the Body object and the destructor releases it. And we may need to think about
what happens if objects of this class are copied or moved.
So I have written a very simple example, this is a very basic Date class.
This is going to be the Body.
So it is going to be the implementation of the Date.
The Handle class will be the interface to Date.
So this is the Date class, as seen by the rest of the code.
It has our pointer to the implementation.
We have the forward declaration of the implementation class, but no other information about it.
In the constructor, we call new, to allocate the implementation pointer, and then we call delete in the
destructor. And then the member functions
just call the corresponding member functions in the Date implementation.
And finally, we have a very simple main() program, to test this out.
And there we are.
So if the client sees this Date_impl in the header and tries to use it, what do you think would
happen?
Undefined class! So the client only knows the name, but it cannot compile against it, because it does not have
the full class definition.
So we have a pointer, and a call to new.
Could we use a unique_ptr here?
And yes, we can.
But there are a couple of things we need to bear in mind.
So we can put a unique_ptr to Body, instead of having a traditional pointer to a Body.
And this will manage the memory allocation for us.
However, this code is not going to compile as it stands.
We need to add a destructor to the Handle class and that will make it a so-called "complete type".
Then if we do have a constructor, that means the compiler is not going to synthesize move operators.
So if we want those, we need to provide them ourselves.
And also, we do not get copy operators, because the class has a member which cannot be copied.
unique_ptr is move-only.
So let's try that out. So we can use the same Date implementation and also the same client. In the Date
header,
we now have a unique_ptr to the implementation.
We also have the destructor. And we have added some move operators.
In the constructor, we call make_unique() to initialize the implementation pointer. And the arguments
for the constructor are going to be forwarded to the implementation.
And then for the destructor and move operators, we can just default those, unless we want to do something
special.
And then we have the same member functions again.
And then we are.
We have already looked at the advantages of this.
We do not need to have the clients including Body.h.
If the implementation changes, we do not need to modify or recompile the clients.
And if we are shipping the implementation as a DLL, then users do not need to install a new program
executable.
It is also useful for hiding the implementation from clients.
So if you have some code which is super secret or commercially sensitive, then no one will be able
to find out what it is.
The disadvantages.
This requires an extra memory allocation for the Body object.
So your program is going to use slightly more memory.
If you call a member function, then it is going to dereference a pointer
to call the Body member function. So that will make your program run slightly more slowly.
Loading a shared library will increase the time that it takes your program to start up. And this approach
makes the codebase more complex.
So where can you use pImpl?
pImpl is often used in large projects, to reduce completion time and simplify product updates. It is also
used a lot by QT, or "cute",
I think it is pronounced. Which is a popular graphics library.
Another possibility is that you could use pImpl to implement the container classes in the standard
template library. For example, with the string class.
This could have a body which stores the character data, and does the actual manipulation of the data.
And then the Handle class, which provides the interface of the string class to the rest of the code.
And if you are very careful, you could arrange for two string objects which share the same data to
share the same Body pointer.
So you could use that for reducing the amount of memory that your program consumes,
if you have a lot of duplicated string objects. Obviously you have to be careful with that, especially
if threads are involved.
But that is something to think about.
Okay.
So that is it for this video.
I will see you next time.
But until then, keep coding!