Like what you hear?
Join forces with Hellebore today!Contact Us
Pointer to Implementation (or PIMPL) is an idiom to abstract a Class's implementation details from its interface by wrapping the implementation details within an opaque pointer. From over a decade of using this idiom, I have gained some insight on when, and when not, to employ it.
At the beginning of my career, I found myself working on a large codebase that would take a significant part of the day to compile. As a junior developer on the team, I was generally tasked with modifying existing code, writing tests, or fixing bugs. If I was not careful, I could end up wasting half my day just waiting for this codebase to compile. Worrying about compilation time caused by modifying a header file was not something I was taught in school. I learned quickly to use alternate debugging techniques, so I was not wasting time making a simple change and spending the rest of the day waiting on code compilation to finish. As part of the team, though, we recognized having to work around long recompilation times due to file changes was not a reasonable nor a productive way to work. So, we decided to investigate solutions that could improve our workflow.
One solution we landed on was the PIMPL idiom. Specifically, the PIMPL implementation written by Herb Sutter in his "Guru of the Week" article [GotW 101]. We integrated this implementation on some small, non-critical piece of code to test it out as we were not very familiar with this technique at the time. We were happy with the results and decided to refactor the rest of our codebase over time. With most of our private interfaces abstracted away to source files, we were now able to make changes to private member functions and member variables without needing to recompile half the codebase. It does not seem like much, but having a large codebase that takes significant time to compile and a multi-developer team that no longer had to wait on recompilation of the code base resulted in improved productivity.
There is a flaw in my previous statement that PIMPL improved my team's productivity. PIMPL improved one aspect of our workflow, but I think it is naïve to say it improved productivity. While it is true that our integration of PIMPL improved recompilation time when modifying private members or private functions. It did not improve compilation time for anything else. Recompilation time improved when modifying this data within the source because changing an algorithm or function parameter in source no longer caused the entire dependency chain of class hierarchies and header files to recompile as the header file was left unchanged. Adding new files, features, tests, and software in general still caused the code base and all dependent code to recompile, which again took significant time.
What improved compilation time and, therefore, productivity was intelligent code architecture, code organization techniques, and modern hardware.
There is a cost to PIMPL that I do not think has been explored very well and which I did not observe until after I had a chance to step away from this project.
The PIMPL idiom is deceptively complex.
The implementation of PIMPL is done so as an opaque pointer. It must be forward declared so that the implementation may be defined in the source file. Forward declaration of a member Class has several side effects, but one, in particular, causes an interesting problem.
A destructor for any class that makes use of PIMPL must be defined. This, in turn, disables the compiler auto-generating default copy and move constructors and operators. So now we have a large complex codebase with hundreds or even thousands of complex classes all needing to adhere to the rule-of-five to perform copy and move semantics successfully. The maintenance requirements for such a task is monumental: Tests will need to be authored. Profiling and benchmarking will be required on critical paths. And new variables require changes in many places for the Class to continue to function correctly.
This does not even account for the suboptimal development of said constructors or assignment operators. Or the amount of time spent on Stack Overflow researching optimal ways of writing these implementations or debugging unexpected compilation or debugging issues that arise from the use of PIMPL. All of this can be avoided if you treat PIMPL for what it is.
The rule-of-zero is a very powerful guideline in keeping your code base simple. Adding PIMPL is the opposite of following the rule-of-zero. This can increase the complexity and cost of your codebase in ways you will not expect or even be able to quantify.
PIMPL is simply another tool in a software engineers toolbox. Recognizing and weighing the benefits of the right tool for the job comes with experience. It has been my experience that PIMPL can be a powerful tool when used sparingly and purposefully. But in excess, it can cause unforeseen consequences to the inexperienced.
Aaron Shelley is a Sr. Engineer with Hellebore Consulting Group, specializing in C++ architecture design and implementation.