The PImpl (Pointer to Implementation) design pattern, also known as Opaque Pointer, emerged as a means to separate the interface and implementation of a class in C++. This is particularly important for maintaining binary interface compatibility (ABI), speeding up compilation, and hiding implementation details from the user of the class.
Background.
In many C++ projects, there is a need to modify the implementations of classes without changing the public interface and without recompiling the clients of those classes. The problem is that any changes in header files require recompilation of all dependent modules, which can be extremely costly in large codebases. PImpl allows minimizing recompilation and provides better encapsulation.
The Issue.
The standard way of defining a class with private members in a header file requires knowledge of all these members during compilation. When extending or changing them, all files that include this header must be recompiled. Furthermore, this exposes implementation details/structure to the client, potentially negatively affecting security and architectural integrity.
The Solution.
PImpl achieves implementation hiding by using a pointer to a forward-declared implementation structure (Impl struct/class) defined in the cpp file. This allows changing the implementation without affecting the interface.
Code example:
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // opaque pointer }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // secrecy inside Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
Key features:
Can std::unique_ptr be used instead of a raw pointer in PImpl?
Yes, the modern and safe approach is to use std::unique_ptr (or std::shared_ptr if shared ownership is required). This allows for proper memory management and avoids explicitly writing a destructor/copy operator for the raw pointer:
private: std::unique_ptr<Impl> pimpl;
Can a class with PImpl be movable but not copyable?
Yes, by providing a move constructor/operator and deleting the copy one. For example:
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
Is there a performance overhead when using PImpl?
Yes, due to pointer dereferencing and additional dynamic memory allocation (heap allocation). For performance-critical structures, this can be a significant downside.
A large company implemented PImpl for all classes, including simple data structures, which led to significant slowdowns in simple operations due to constant pointer dereferencing.
Pros:
Cons:
In a project with a long-lived user interface library, PImpl was applied only for complex widgets with frequently changing internals, maintaining stable ABI for external clients.
Pros:
Cons: