Modern C++: The Pimpl Idiom

A Hidden Gem for Better Designs

Dagang Wei
3 min readJun 24, 2024

This blog post is part of the series Modern C++.

Introduction

Welcome back, C++ coders! In today’s post, we’ll dive into an often-overlooked technique that can significantly enhance your C++ code’s maintainability, compilation times, and encapsulation — the Pimpl Idiom.

The Challenges of Traditional Class Design

Before we dive into Pimpl, let’s revisit some common issues that arise with traditional class designs, illustrated with a simple example:

// image_processor.h (header file)
#include <vector>
#include <third_party_lib.h> // Imagine a large external library

class ImageProcessor {
public:
ImageProcessor();
void process(const std::vector<unsigned char>& imageData);

private:
ThirdPartyObject complexInternalObject;
int width, height;
std::vector<unsigned char> processedData;
};

Even in this simple example, we can identify several potential problems:

  • Compilation Bottlenecks: Any file that includes image_processor.h will need to recompile if we change the definition of ThirdPartyObject or even just the order of private members.
  • Leaky Encapsulation: The header reveals internal details like the use of ThirdPartyObject, which might be an implementation choice we want to keep flexible.
  • Rigidity and Tight Coupling: Our code is now directly tied to third_party_lib.h. If this library changes, our code might break.

The Pimpl Idiom to the Rescue

The Pimpl Idiom is designed to address these challenges by cleverly separating a class’s interface from its implementation. Let’s refactor our ImageProcessor using Pimpl.

Header:

// image_processor.h
#include <memory>
#include <vector>

class ImageProcessor {
public:
ImageProcessor();
~ImageProcessor(); // Important for cleaning up pImpl
void process(const std::vector<unsigned char>& imageData);

private:
class Impl; // Forward declaration
std::unique_ptr<Impl> pImpl;
};

Implementation:

// image_processor.cpp
#include "image_processor.h"
#include <third_party_lib.h>

class ImageProcessor::Impl {
public:
void process(const std::vector<unsigned char>& imageData) {
// ... (Actual image processing logic using ThirdPartyObject)
}

private:
ThirdPartyObject complexInternalObject;
// ... other private members
};

ImageProcessor::ImageProcessor() : pImpl(std::make_unique<Impl>()) {}

ImageProcessor::~ImageProcessor() = default;

void ImageProcessor::process(const std::vector<unsigned char>& imageData) {
pImpl->process(imageData);
}

Now, changes to the implementation (e.g., how we use ThirdPartyObject) won't force recompilation of code that just uses ImageProcessor. We've also hidden the implementation details in image_processor.cpp.

Why Use Pimpl?

Faster Compilation:

  • Reduced Header Dependencies: Changes in the implementation details won’t trigger recompilation of files that include the handle class’s header. This is especially valuable in large projects.
  • Minimal Recompilation Cascade: Modifications are confined to the implementation file, speeding up your build process.

Improved Encapsulation:

  • Hidden Implementation: The handle class’s header reveals only the public interface. Internal data structures and implementation-specific headers are kept out of sight.
  • ABI Stability: The layout of the handle class remains stable, even if its implementation changes. This prevents compatibility issues in shared libraries or when distributing headers to clients.

Flexible Design:

  • Easy Refactoring: You can change internal implementation details without affecting client code that uses the handle class.
  • Dependency Management: You can isolate third-party library dependencies to the implementation file.

When to Use Pimpl?

The Pimpl Idiom is a great fit when:

  • You have a class with complex internal details that you want to hide from the user.
  • Compilation times are a concern in your project.
  • You need ABI stability for shared libraries or distributed headers.
  • You anticipate frequent changes to a class’s internal implementation.

Implementing Pimpl: A Modern C++ Approach

Modern C++ makes implementing the Pimpl idiom quite elegant:

  • Forward Declarations: Use forward declarations in the header file to avoid including the implementation header.
  • Smart Pointers: Use std::unique_ptr to manage the lifetime of the pImpl object.
  • Rule of Five: Consider implementing the Rule of Five (copy/move constructors, copy/move assignment operators) if your class needs custom copy/move semantics.

Pimpl and C++ Modules (C++20)

In C++20 and beyond, modules offer an alternative way to manage dependencies and compile times. While modules are a powerful tool, Pimpl remains relevant in scenarios where you need to maintain ABI compatibility or desire strict encapsulation boundaries.

Happy Coding!

Reference

--

--