Certain class implementations carry with them a lot of compile-time dependencies, and making changes to those will require recompilation of everything that uses a representation of the class. The pImpl (pointer to Implementation) pattern alleviates compile-time dependencies by moving all the implementation-level details of a class into a different class that is accessible via an opaque pointer, which is a fancy name for a pointer that specifically hides implementation details from the caller.
In what use cases would someone need to utilize this pattern? Sometimes, the inclusion of external libraries and dependencies carries unwanted baggage: for instance, the infamous Windows.h library includes macros that can pollute the global scope and introduce undesired behavior. Using the pImpl pattern for a class that must leverage Windows.h would ensure that all the implementation details are hidden away, preventing propagation into other areas of the code.
Other use cases include ABI (application binary interface) stability for libraries. This essentially ensures that the forward-facing binary interface for a library would remain largely intact when a behind-the-scenes implementation change occurs. The public would be none the wiser since all the implementation details for the class are hidden anyways.
The standard implementation of the pattern would look like so:
once <memory> <cstdint>class Firewall { public: /// Default Constructor Firewall(); /// Default Destructor ~Firewall(); /// Copy constructor Firewall(Firewall const&); /// Copy assignment Firewall& operator=(Firewall const&); /// Move constructor Firewall(Firewall&&) noexcept; /// Move assignment Firewall& operator=(Firewall&&) noexcept; /// Simple mutator void SetData(uint64_t data); /// Simple accessor uint64_t GetData() const; private: /// Forward declaration of the implementation class class impl; /// Opaque pointer to implementation details std::unique_ptr<impl> pimpl_;};
"firewall.hpp"class Firewall::impl { public: uint64_t data_;};/// Firewall class ctorFirewall::Firewall() : pimpl_(std::make_unique<impl>()) {}/// Firewall class dtorFirewall::~Firewall() = default;/// Copy constructorFirewall::Firewall(Firewall const& other) : pimpl_(std::make_unique<impl>(*other.pimpl_)) {}/// Copy assignmentFirewall& Firewall::operator=(Firewall const& rhs) { if (this != &rhs) { pimpl_.reset(new Firewall::impl(*rhs.pimpl_)); } return *this;}/// Move constructorFirewall::Firewall(Firewall&& other) noexcept = default;/// Move assignmentFirewall& Firewall::operator=(Firewall&& other) noexcept = default;/// Accessoruint64_t Firewall::GetData() const { return pimpl_->data_;}/// Mutatorvoid Firewall::SetData(uint64_t data) { pimpl_->data_ = data;}
<iostream> "firewall.hpp"int main() { Firewall f1; f1.SetData(100); Firewall f2 = f1; Firewall f3; f3 = f2; Firewall f4 = std::move(f1); Firewall f5; f5 = std::move(f3); std::cout << "f2: " << f2.GetData() << std::endl; std::cout << "f4: " << f4.GetData() << std::endl; return 0;}
Wow. That is a lot of code for a simple class, especially when compared to a pattern like the singleton. The complexity of implementing the pattern alone may be daunting enough to drive some developers away. The example provided above is a very bare-bones version of the pattern; more robust (and consequently more complicated) versions exist.
Benefits
The example class provided above is named Firewall in reference to the fact that the pImpl pattern creates a “compilation firewall.” This was briefly discussed at the beginning of the article, but what this implies is that changes to the implementation-level details of the class do not require recompilation of the code that references or uses the class.
Another pro is again something else touched on previously: binary compatibility. When updating a library that uses this pattern, as long as the ABI remains the same, the end user will be able to link to the latest version of the library with no issue.
Drawbacks
The obvious issue here is a hit to runtime performance. The main benefits to using this pattern are realized at compile-time, so naturally this inversely affects runtime. The reason this occurs is that by hiding all the class implementation details behind an opaque pointer, a layer of indirection is added to the class. This means that, to access or modify something within the class, all operations must first be accessed via the unique pointer to the implementation. This may not affect overall runtime that much, but for time-sensitive applications, it may be enough to stay away.
The complexity of the code by itself is a drawback; this pattern is more involved than other C++ design patterns, and at first glance, it can be confusing to understand what is going on. Maintenance quickly becomes a factor as well. Having multiple classes that implement pImpl could become overwhelming without proper documentation and understanding of the code.
Debugging can be a nightmare as well since the class is split. Determining where an issue is occurring could be cumbersome, even with tools such as gdb. Testing might be a problem, although most testing would focus on the front-facing interface.
As with all patterns, even if implemented correctly, it can very easily be overused. This pattern is great for libraries that experience frequent updates that don’t affect their ABI; aside from that, the benefits are up to the discretion of the developer. Personally, I have worked with code that has unnecessarily implemented the pImpl pattern, and it can be overwhelming to read through and understand what the class should be doing.
If you enjoy learning about design patterns, or have suggestions for other discussions / examples, leave a comment below, and make sure to subscribe to email notifications for future articles.