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.

The Singleton Pattern in C++

A singleton is a class that does not allow for dynamic instantiation. At runtime, one instance of the class is statically initialized, and this instance cannot be copied or assigned. To use the object, an accessor method is provided within the class definition to allow another portion of the program to grab a reference. Here is an example class definition that uses this pattern:

// singleton.h
#include <string>

class Singleton {
 public:
  /// Deleted Singleton Copy Constructor
  Singleton(Singleton const&) = delete;

  /// Deleted Singleton Assignment Operator
  void operator=(Singleton const&) = delete;

  /// Static accessor method
  static Singleton& Instance() {
    static Singleton singleton;
    return singleton;
  }
  
  void SetValue(const std::string& str) {
    this.value_ = str;
  }

  std::string GetValue() {
    return value_;
  }

 private:
  /// Privatizing the Singleton default constructor
  Singleton() = default;
  /// Dummy member field for demonstration
  std::string value_;
}

After defining a class with the singleton pattern, it can be used like so:

// main.cpp
#include <iostream>
#include "singleton.h"

int main() {
  auto singleton = Singleton::Instance();
  singleton.SetValue("Hello World!");
  std::cout << singleton.GetValue() << std::endl;
  return 0;
}

The main.cpp file calls Singleton::Instance() which returns a reference to the statically initialized singleton object and assigns the reference to singleton. From there, we now have a reference to the static singleton object that we can use to access or mutate its member fields. Calling SetValue() allows us to pass in a std::string to mutate the singleton’s value_ member variable. Calling GetValue() returns the now-modified member variable and prints it to stdout.

Benefits

The singleton pattern is great for instances where multiple parts of a program need global access to a single resource. Such examples would include a global configuration service or a database management interface; there is hardly a necessity to have multiple copies of such classes, so it would be better to have one instance that is globally accessible to all portions that require its use.

This pattern can also prevent unnecessary memory allocation; by making it impossible to create more than one instance of a class that uses this pattern, it is guaranteed that memory will not have to be allocated more than once for the class (of course, this does not take into account possible memory allocations for dynamically resizable and allocatable member fields).

By ensuring one instance of a class, the program only has one interface for interacting with and accessing the resource. This alleviates having to deal with multiple interfaces or representations of a particular type; calling the Instance method for the class is enough.

Drawbacks

Shared, globally accessible resources can be great for single-threaded programs, but multi-threaded applications can quickly run into race condition issues. To counteract this, the singleton would need to be made thread-safe, but this can increase the complexity of the class design.

Design patterns are great for easily replicable code, but some patterns can be overused or incorrectly used. The singleton pattern can oftentimes be overused; sometimes, a simple, standard class definition would suffice. It is critical that this pattern only be leverage when it is deemed truly necessary.

Nonetheless, the singleton, if used correctly, can greatly improve the design and flow of a program. The benefits often outweigh the drawbacks, but as with any design pattern, it is up to the developer to analyze and determine its effectiveness and applicability to the task at hand.

Basic Enumerations

An enumeration, or enum in Rust, is a way to declare a custom data type that describes, or enumerates, possible variations of a certain category. In the example below, the data type is named Language, and it serves as the category for which all the named items within are a part. EnglishSpanish, etc. are all a member of the Language enumeration. Enumerations are implicitly given an integer value corresponding with their ordinal position within the definition, starting with 0. Unless expressly overridden, the numerical value of each member increments by 1. These types of enumerations are classified as unit-only enums. Other types of enumerations are described in detail in the Advanced Enumerations section.

// Unit-only enumeration
enum Language {
  English,  // = 0
  Spanish,  // = 1
  Italian,  // = 2
  French,   // = 3
  German,   // = 4
}
// Overridden unit-only enumeration
enum Country {
  America = 4,
  Germany,  // = 5
  France,   // = 6
  Austria = 8,
}