Special Functions And Pimpl Idiom

Published: (March 6, 2026 at 06:30 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Benefits

The benefits come in the form of lowered compilation times and easier header management.
Compilation times decrease because, instead of declaring every member in a header file (which forces us to include the corresponding headers), we hide the members behind a pointer. For example, if we have members of type std::vector and std::string, we would normally need to include their headers. Any change to those member types—or to a custom type that is revised frequently—would otherwise trigger recompilation of the entire header for our class.

Use Pointers

As the name suggests, we hold a pointer to the implementation. The pointer can be a raw pointer, a std::unique_ptr, or a std::shared_ptr.
Raw and unique pointers are the most performance‑friendly, so we’ll look at those first.

Pointer

// Header file
class Widget
{
public:
    Widget();
    ~Widget();

private:
    struct Impl;               // forward declaration (incomplete type)
    Impl* pImpl;                // pointer to implementation
};
// Source file
struct Widget::Impl
{
    std::vector    m_age;
    std::string    m_name;
    PlayerController    m_controller;   // custom type
};

Widget::Widget()
{
    // Constructor
    pImpl = new Impl();
}

Widget::~Widget()
{
    delete pImpl;
}

Unique Pointer

Before diving into code, note a few base points about the Pimpl idiom:

  • It hides private data, keeping the public API stable.
  • The header only contains a small pointer (typically 4 or 8 bytes), so changes to the implementation do not force recompilation of the header.
  • The indirection introduces a slight runtime overhead.
  • Special care is needed when defining copy operations.

This approach is well‑suited for classes with a large number of private members.

// Header file
class Person
{
public:
    Person(std::string name, std::string address);
    ~Person();

    // Copy operations
    Person(const Person& rhs);
    Person& operator=(const Person& rhs) = delete;

    // Move operations
    Person(Person&& rhs);
    Person& operator=(Person&& rhs) = delete;

    std::string GetAttributes() const;

private:
    struct Impl;
    std::unique_ptr pImpl;
};
// Source file
// Define the structure that holds the member data
struct Person::Impl
{
    std::string name;
    std::string address;
};

Person::Person(std::name, std::address) : pImpl{std::make_unique()}
{
    pImpl->name    = name;
    pImpl->address = address;
}

Person::~Person() = default;

Person::Person(const Person& rhs)
{
    pImpl = std::make_unique();
    pImpl->name    = rhs.pImpl->name;
    pImpl->address = rhs.pImpl->address;
}

Person::Person(Person&& rhs) noexcept
{
    pImpl = std::move(rhs.pImpl);
}

std::string Person::GetAttributes() const
{
    return pImpl->name + '\t' + pImpl->address;
}

A few things to note

  • The implementation struct must be defined before any constructor that uses it; otherwise the compiler will complain because std::unique_ptr cannot manage an incomplete type.
  • Declaring a destructor suppresses the compiler‑generated move operations, so you must define them yourself if needed.
  • With a unique pointer you need to provide a custom copy constructor; the default would perform a shallow copy.

Shared Pointer

Using std::shared_ptr for the Pimpl idiom is less common, but it eliminates the need to write custom copy or move operations because a shared pointer is copyable and movable by nature. It also works with incomplete types, so the implementation struct can remain forward‑declared in the header.

0 views
Back to Blog

Related posts

Read more »