Skip to main content

Updated Jun 4, 2026

Pimpl (Pointer to Implementation)

Pimpl (Pointer to Implementation) separates a class's interface from its implementation by moving private members into a separate implementation class. This reduces compilation dependencies and provides better encapsulation.

The Problem​

Without Pimpl, changing private members forces recompilation of all files that include the header, even though they can't access those members.

// widget.h - WITHOUT Pimpl
#include "detail.h" // Implementation dependency
#include "helper.h" // Leaked to users
#include <vector>

class Widget {
public:
void doSomething();

private:
Detail detail_; // Exposes Detail to users
Helper helper_; // Exposes Helper to users
std::vector<int> data_; // Forces vector recompilation
};

// Any change to private members requires recompiling all users!

The Solution​

Pimpl hides implementation details behind an opaque pointer, making the interface header independent of implementation changes.

// widget.h - WITH Pimpl
#include <memory>

class Widget {
public:
Widget();
~Widget();

// Rule of Five needed with Pimpl
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
Widget(const Widget&);
Widget& operator=(const Widget&);

void doSomething();

private:
struct Impl; // Forward declaration
std::unique_ptr<Impl> pImpl_;
};

// widget.cpp
#include "widget.h"
#include "detail.h" // Only needed in .cpp
#include "helper.h" // Only needed in .cpp
#include <vector>

struct Widget::Impl {
Detail detail;
Helper helper;
std::vector<int> data;
};

Widget::Widget() : pImpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

Widget::Widget(const Widget& other)
: pImpl_(std::make_unique<Impl>(*other.pImpl_)) {}

Widget& Widget::operator=(const Widget& other) {
*pImpl_ = *other.pImpl_;
return *this;
}

void Widget::doSomething() {
pImpl_->helper.help();
pImpl_->data.push_back(42);
}
info

The destructor must be defined in the .cpp file where Impl is complete, otherwise unique_ptr can't delete it.

Benefits​

Pimpl provides several compile-time and design advantages.

Faster Compilation​

Only the implementation file needs recompilation when private members change, not all users of the class.

// Changing Widget::Impl only requires recompiling widget.cpp
// All files that include widget.h don't need recompilation

Better Encapsulation​

Implementation details are completely hidden from users - they can't even see the types used internally.

// widget.h - users can't see Detail, Helper, or vector
class Widget {
struct Impl;
std::unique_ptr<Impl> pImpl_;
};

ABI Stability​

The class layout doesn't change when private members change, maintaining binary compatibility across library versions.

// Adding members to Impl doesn't break ABI
// Widget's memory layout stays the same

Reduced Header Dependencies​

Implementation headers are only included in .cpp files, reducing transitive includes for users.

// widget.h - minimal includes
#include <memory> // Only this needed

// widget.cpp - heavy includes hidden
#include "complex_implementation.h"
#include "third_party_library.h"

Implementation Patterns​

Different use cases require different Pimpl implementations.

Basic Pimpl (Move-Only)​

Simplest form - no copying, only moving, reduces boilerplate code.

// widget.h
class Widget {
public:
Widget();
~Widget();
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;

private:
struct Impl;
std::unique_ptr<Impl> pImpl_;
};

// widget.cpp
struct Widget::Impl {
// Implementation members
};

Widget::Widget() : pImpl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

Copyable Pimpl​

Enables copying by implementing copy constructor and assignment with deep copy.

// widget.h
class Widget {
public:
Widget();
~Widget();

Widget(const Widget& other);
Widget& operator=(const Widget& other);

Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;

private:
struct Impl;
std::unique_ptr<Impl> pImpl_;
};

// widget.cpp
Widget::Widget(const Widget& other)
: pImpl_(std::make_unique<Impl>(*other.pImpl_)) {}

Widget& Widget::operator=(const Widget& other) {
if (this != &other) {
*pImpl_ = *other.pImpl_;
}
return *this;
}

Shared Pimpl​

Multiple objects can share the same implementation using reference counting.

// widget.h
class Widget {
public:
Widget();
// Compiler-generated copy/move work correctly

private:
struct Impl;
std::shared_ptr<Impl> pImpl_;
};

// widget.cpp
Widget::Widget() : pImpl_(std::make_shared<Impl>()) {}

Fast Pimpl (Inline Storage)​

Avoids heap allocation by using inline storage, trading header changes for performance.

// widget.h
class Widget {
public:
Widget();
~Widget();

private:
static constexpr size_t STORAGE_SIZE = 64;
alignas(std::max_align_t) std::byte storage_[STORAGE_SIZE];

struct Impl;
Impl* pImpl_;
};

// widget.cpp
struct Widget::Impl {
// Must fit in STORAGE_SIZE
};

Widget::Widget() {
static_assert(sizeof(Impl) <= STORAGE_SIZE);
pImpl_ = new(storage_) Impl();
}

Widget::~Widget() {
pImpl_->~Impl();
}

Practical Examples​

Example 1: Network Client​

Network implementation details are hidden, allowing protocol changes without recompiling users.

// client.h
#include <memory>
#include <string>

class NetworkClient {
public:
NetworkClient(const std::string& host, int port);
~NetworkClient();

NetworkClient(NetworkClient&&) noexcept;
NetworkClient& operator=(NetworkClient&&) noexcept;

bool connect();
void send(const std::string& data);
std::string receive();

private:
struct Impl;
std::unique_ptr<Impl> pImpl_;
};

// client.cpp
#include "client.h"
#include <sys/socket.h> // Not leaked to users
#include <netinet/in.h> // Not leaked to users
#include <vector>

struct NetworkClient::Impl {
int sockfd;
sockaddr_in serverAddr;
std::vector<char> buffer;

Impl(const std::string& host, int port);
~Impl();
};

NetworkClient::NetworkClient(const std::string& host, int port)
: pImpl_(std::make_unique<Impl>(host, port)) {}

NetworkClient::~NetworkClient() = default;
NetworkClient::NetworkClient(NetworkClient&&) noexcept = default;
NetworkClient& NetworkClient::operator=(NetworkClient&&) noexcept = default;

bool NetworkClient::connect() {
return ::connect(pImpl_->sockfd,
(sockaddr*)&pImpl_->serverAddr,
sizeof(pImpl_->serverAddr)) == 0;
}

Example 2: Database Connection​

Database-specific headers are isolated, preventing pollution of user code with database types.

// database.h
#include <memory>
#include <string>
#include <vector>

class Database {
public:
Database(const std::string& connectionString);
~Database();

bool connect();
void execute(const std::string& query);
std::vector<std::string> fetchResults();

private:
struct Impl;
std::unique_ptr<Impl> pImpl_;
};

// database.cpp
#include "database.h"
#include <libpq-fe.h> // PostgreSQL - not leaked

struct Database::Impl {
PGconn* conn;
PGresult* result;
std::string connString;

Impl(const std::string& cs) : conn(nullptr), result(nullptr), connString(cs) {}

~Impl() {
if (result) PQclear(result);
if (conn) PQfinish(conn);
}
};

Database::Database(const std::string& connectionString)
: pImpl_(std::make_unique<Impl>(connectionString)) {}

Database::~Database() = default;

bool Database::connect() {
pImpl_->conn = PQconnectdb(pImpl_->connString.c_str());
return PQstatus(pImpl_->conn) == CONNECTION_OK;
}

Example 3: GUI Widget​

Platform-specific GUI code is hidden, enabling cross-platform interfaces without exposing platform details.

// button.h
#include <memory>
#include <string>
#include <functional>

class Button {
public:
Button(const std::string& text);
~Button();

void setText(const std::string& text);
void setOnClick(std::function<void()> callback);
void show();

private:
struct Impl;
std::unique_ptr<Impl> pImpl_;
};

// button_windows.cpp (Windows platform)
#include "button.h"
#include <windows.h> // Not leaked to users

struct Button::Impl {
HWND hwnd;
std::function<void()> onClick;

Impl(const std::string& text) {
hwnd = CreateWindow("BUTTON", text.c_str(),
WS_VISIBLE | WS_CHILD,
10, 10, 100, 30, NULL, NULL, NULL, NULL);
}

~Impl() {
DestroyWindow(hwnd);
}
};

// button_linux.cpp (Linux platform)
#include "button.h"
#include <gtk/gtk.h> // Not leaked to users

struct Button::Impl {
GtkWidget* widget;
std::function<void()> onClick;

Impl(const std::string& text) {
widget = gtk_button_new_with_label(text.c_str());
}

~Impl() {
gtk_widget_destroy(widget);
}
};

Performance Considerations​

Pimpl has trade-offs between compilation time and runtime performance.

Extra Indirection​

Every access through pImpl requires pointer dereference, adding slight overhead.

// One extra indirection
void Widget::doSomething() {
pImpl_->data.push_back(42); // pImpl_ -> data -> push_back
}

// vs direct access
void Widget::doSomething() {
data_.push_back(42); // data_ -> push_back
}

Heap Allocation​

Creating a Pimpl object requires heap allocation, which is slower than stack allocation.

// Heap allocation
Widget w; // Allocates Impl on heap

// Can be mitigated with inline storage
// or object pools

Cache Locality​

Implementation data is not contiguous with the object, potentially affecting cache performance.

// Poor cache locality
std::vector<Widget> widgets(1000);
for (auto& w : widgets) {
w.doSomething(); // Each access jumps to different heap location
}

When to Use Pimpl​

Pimpl is most beneficial in specific scenarios, not everywhere.

Use Pimpl When:
  • Class has complex private implementation
  • Reducing compilation dependencies is important
  • Providing stable ABI for library interface
  • Hiding platform-specific implementation
  • Private members change frequently
warning

Avoid Pimpl When:

  • Performance is critical (hot path code)
  • Class is header-only template
  • Implementation is simple (few dependencies)
  • Creating many short-lived objects
  • Overhead outweighs compilation benefits

Best Practices​

Follow these guidelines for effective Pimpl usage.

DO:
  • Define destructor in .cpp file
  • Use unique_ptr for exclusive ownership
  • Forward declare Impl in header
  • Include Impl definition only in .cpp
  • Implement Rule of Five if needed
danger

DON'T:

  • Define destructor inline in header
  • Use Pimpl for every class
  • Expose Impl pointer to users
  • Forget to implement copy operations
  • Mix Pimpl and inline implementations

Pimpl Variants​

Different pointer types provide different semantics and performance characteristics.

VariantPointer TypeSemanticsCopy Behavior
Uniqueunique_ptrExclusive ownershipMove-only or deep copy
Sharedshared_ptrShared ownershipShallow copy
FastInline storageNo allocationComplex copy