The Pimpl Idiom

One of the strengths of OOP is the ability to separate the interface from the implementation. For example, if you are building a cross-platform library, the interface should be common across all platforms and ideally the API should not bring in any platform-specific definitions or header files.

This can be awkward when you want to hold on to some private data such as a handle to a window or some other platform-specific object.

For example, in Windows, a handle to a window is represented by the HWND type. To store an HWND as a private member in your class, you will need to include windows.h in your header file. However, ideally we would like the interface to our class to be free from such dependencies.

To overcome this issue, we shall introduce the Pimpl idiom. Pimpl is short for ‘pointer to implementation’ and is a popular method for implementing ‘opaque pointers’ in C++. There are many ways of implementing Pimpl in C++ – we shall just demonstrate one such approach.

Example – Application settings

In Windows, it is common to store application settings in the registry. For example, if we are writing a game, we might want to keep track of the high score by storing it in the registry. For this example, we shall store this data in the following registry key:

HKEY_CURRENT_USER\SOFTWARE\PimplDemo

Header file Settings.h:

#pragma once

#include <memory>

class SettingsImpl;

class Settings
{
public:
    Settings();
    virtual ~Settings();

    Settings& operator=(const Settings& rhs) = delete;
    Settings(const Settings& other) = delete;

    int getHighScore();

private:
    std::unique_ptr<SettingsImpl> pImpl;
};

Implementation file Settings.cpp:

#include <Windows.h>
#include <tchar.h>
#include <assert.h>
#include <string>
#include "Settings.h"

class SettingsImpl
{
public:
    DWORD getDWORD(const TCHAR * property)
    {
        DWORD value = 0;
        DWORD bufferSize = sizeof(value);
        DWORD dataType; // REG_DWORD
        LSTATUS rc = RegQueryValueEx(hKey, property, NULL, &dataType, (BYTE*)& value, &bufferSize);
        assert(dataType == REG_DWORD);
        return value;
    }

public:
    HKEY hKey = nullptr;
};

Settings::Settings() : pImpl(std::make_unique<SettingsImpl>())
{
    LSTATUS rc = RegOpenKeyEx(HKEY_CURRENT_USER,
        _T("SOFTWARE\\PimplDemo"), NULL, KEY_READ, &pImpl->hKey);
}

Settings::~Settings()
{
}

int Settings::getHighScore()
{
    DWORD highScore= pImpl->getDWORD(TEXT("HighScore"));
    return (int)highScore;
}

Review

Some of the benefits of this approach are:

  • No platform-specific definitions in the header file.
  • Minimal inclusion of header files in Settings.h can help with compilation speeds.
  • Inner workings of the class (such as getDWORD()) are completely hidden, potentially reducing the amount of recompilation.
  • No unsafe casts (e.g. hiding data with a void* pointer).
  • Debugger-friendly – it is still possible to see the inner mechanics of SettingsImpl in the debugger.