Coding with Acne: The pImpl Pattern

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:

// firewall.hpp
#include <memory>
#include <cstdint>

class Firewall {
 public:
  /// Default Constructor
  Firewall();
  /// Explicit Constructor
  explicit Firewall(uint64_t);
  /// Default Destructor
  ~Firewall();

  /// Copy constructor
  Firewall(Firewall const&);
  /// Assignment operator
  Firewall& operator=(Firewall const&);

  /// Simple mutator
  void SetData(uint64_t data);
  /// Simple accessor
  uint64_t GetData();

 private:
  /// Forward declaration of the implementation class
  class impl;
  /// Opaque pointer to implementation details
  std::unique_ptr<impl>> pimpl_;
};
// firewall.cpp
#include "firewall.hpp"

class Firewall::impl {
 public:
  /// Constructor
  explicit impl(uint64_t data) : data_(data){};
  /// Destructor
  ~impl();
 private:
  uint64_t data_;
};

/// Firewall class ctor
Firewall::Firewall(uint64_t data) : pimpl_(new impl(data)) {}
/// Firewall class dtor
Firewall::~Firewall() = default;

/// Copy constructor
Firewall::Firewall(Firewall const& other)
    : pimpl_(new impl(*other.pimpl_)) {}

/// Assignment operator
Firewall& Firewall::operator=(Firewall const& rhs) {
  std::swap(pimpl_, rhs.pimpl_);
  return *this;
}

/// Accessor
uint64_t Firewall::GetData() {
  return pimpl_->data_;
}

/// Mutator
void Firewall::SetData(uint64_t data) {
  pimpl_->data_ = data;
}

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.

Leave a comment