Special Functions And Pimpl Idiom
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_ptrcannot 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.