Menu
Coddy logo textTech

Pimpl Idiom

Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 78 of 104.

The Pimpl idiom (Pointer to Implementation) is a technique that hides a class's implementation details by moving them into a separate, forward-declared class. This reduces compilation dependencies and keeps private members truly hidden from the header file.

The core idea is simple: instead of declaring private members directly in your class, you declare a pointer to an implementation class that's defined only in the source file:

// Widget.h
#include <memory>

class Widget {
public:
    Widget();
    ~Widget();
    void doSomething();
    
private:
    class Impl;  // Forward declaration
    std::unique_ptr<Impl> pImpl;
};
// Widget.cpp
#include "Widget.h"
#include <iostream>

class Widget::Impl {
public:
    int data = 42;
    void process() { std::cout << "Processing: " << data << "\n"; }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;

void Widget::doSomething() { pImpl->process(); }

The key benefits are compilation firewall and binary compatibility. When you change the Impl class, only the source file needs recompilation - not every file that includes the header. This dramatically speeds up build times in large projects.

Note that the destructor must be defined in the source file (even if defaulted) because unique_ptr needs the complete type of Impl to delete it. This is a common pitfall when first using Pimpl.

challenge icon

Challenge

Easy

Let's build a secure message handler using the Pimpl idiom to hide the encryption implementation details from the header file. This demonstrates how Pimpl creates a compilation firewall—anyone including your header won't see the internal workings of your message processing.

You'll organize your code across three files:

  • SecureMessage.h: Define the public interface for your SecureMessage class.

    Your class should have a forward-declared Impl class and a std::unique_ptr to it. The public interface should include:

    • A constructor that takes a const std::string& for the original message
    • A destructor (must be declared here, defined in the .cpp file)
    • A setKey(int key) method to set an encryption key
    • A getEncrypted() method that returns the encrypted message as a std::string
    • A getOriginal() method that returns the original message

    The header should only show the public interface—no implementation details about how encryption works should be visible here.

  • SecureMessage.cpp: Define the nested Impl class and implement all methods.

    Your Impl class should store the original message, the encryption key (default to 0), and handle the actual encryption logic. Use a simple Caesar cipher for encryption: shift each character by the key value. For example, with key 3, 'a' becomes 'd', 'z' wraps around to 'c'.

    The encryption should only affect lowercase letters (a-z), leaving all other characters unchanged. Remember to define the destructor here (even if defaulted) since unique_ptr needs the complete Impl type.

  • main.cpp: Read two inputs:
    1. A message string (may contain spaces)
    2. An encryption key (integer)

    Create a SecureMessage object, set the key, and display the results:

    1. Print Original: followed by the original message
    2. Print Encrypted: followed by the encrypted message

For example, with inputs hello world and 3:

Original: hello world
Encrypted: khoor zruog

With inputs xyz abc and 5:

Original: xyz abc
Encrypted: cde fgh

Notice how the header file reveals nothing about the Caesar cipher implementation—that's the power of Pimpl. If you later changed to a different encryption algorithm, only SecureMessage.cpp would need recompilation, not any file that includes the header.

Cheat sheet

The Pimpl idiom (Pointer to Implementation) hides implementation details by moving them to a separate class defined only in the source file, creating a compilation firewall.

Basic structure with forward declaration in header:

// Widget.h
#include <memory>

class Widget {
public:
    Widget();
    ~Widget();  // Must be declared in header, defined in .cpp
    void doSomething();
    
private:
    class Impl;  // Forward declaration
    std::unique_ptr<Impl> pImpl;
};

Implementation in source file:

// Widget.cpp
#include "Widget.h"

class Widget::Impl {
public:
    int data = 42;
    void process() { /* implementation */ }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;  // Must be defined in .cpp

void Widget::doSomething() { pImpl->process(); }

Key points:

  • The destructor must be defined in the source file (even if defaulted) because unique_ptr needs the complete type of Impl to delete it
  • Changes to Impl only require recompiling the source file, not files that include the header
  • Provides compilation firewall and binary compatibility

Try it yourself

#include <iostream>
#include <string>
#include "SecureMessage.h"

int main() {
    // Read the message (may contain spaces)
    std::string message;
    std::getline(std::cin, message);
    
    // Read the encryption key
    int key;
    std::cin >> key;
    
    // TODO: Create a SecureMessage object with the message
    
    // TODO: Set the encryption key
    
    // TODO: Print "Original: " followed by the original message
    
    // TODO: Print "Encrypted: " followed by the encrypted message
    
    return 0;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming