The PImpl idiom and the C++26 std::indirect type
Skip to content
PImpl (which stands for Pointer to implementation) is a programming tehnicque to remove implementation details from a class by placing them in a separate class that is accessed through an opaque pointer. Its purpose is to separate interfaces and implementations and minimize compile-time dependencies. In this article, we’ll at how the PImpl idiom is typically implemented in C++ and how C++26 simplifies its implementation.
Implementing with a raw pointer
We can implement the Pimpl idiom using raw pointers and abiding to the Rule of Five. This is a guideline that says that when a class defines a special member function (for resource management) it should define all five of them (destructor, copy constructor, copy assignment operator, move constructor, move assignment operator).
To demonstrate this, we will use a widget that represents a UI element that has a label and can be clicked and on each click a counter is incremented. Therefore, a widget would have a counter and a label but these are implementation details that are hidden behind an implementation class.
This Widget class definition looks as follows:
#pragma once
#include
class Widget<br>public:<br>Widget(const std::string& name);<br>~Widget();
Widget(const Widget& other);<br>Widget& operator=(const Widget& other);
Widget(Widget&& other) noexcept;<br>Widget& operator=(Widget&& other) noexcept;
void click();<br>int clickCount() const;<br>std::string label() const;
private:<br>struct Impl;<br>Impl* pimpl_;<br>};
The Widget::Impl class is an incomplete type here. It’s forward declared and defined in the .cpp file. The Widget class keeps a pointer to an object of this type.
#include "Widget.h"
struct Widget::Impl<br>std::string name;<br>int clicks = 0;
explicit Impl(std::string n) : name(std::move(n)) {}<br>};
Widget::Widget(const std::string& name)<br>: pimpl_(new Impl(name))
Widget::~Widget()<br>delete pimpl_;
Widget::Widget(const Widget& other)<br>: pimpl_(new Impl(*other.pimpl_))
Widget& Widget::operator=(const Widget& other)<br>if (this != &other)<br>Impl* tmp = new Impl(*other.pimpl_);<br>delete pimpl_;<br>pimpl_ = tmp;<br>return *this;
Widget::Widget(Widget&& other) noexcept<br>: pimpl_(other.pimpl_)<br>other.pimpl_ = nullptr;
Widget& Widget::operator=(Widget&& other) noexcept<br>if (this != &other)<br>delete pimpl_;<br>pimpl_ = other.pimpl_;<br>other.pimpl_ = nullptr;<br>return *this;
void Widget::click() { ++pimpl_->clicks; }<br>int Widget::clickCount() const { return pimpl_->clicks; }<br>std::string Widget::label() const { return pimpl_->name; }
The Widget class defines all five special member functions. The move constructor and assignment operator are defined noexcept because they just copy/delete objects and nothing should throw. Moreover, it’s a performance issue because a container such as std::vector will only move elements during reallocation if the move constructor is noexcept; otherwise it falls back to copying to preserve its strong exception guarantee.
The Impl object is created during the construction of the Widget and basically stores the state of the widget. The public interface methods of Widget use it to access the state (clicks, name).
A Widget can be used as follows:
#include<br>#include
#include "Widget.h"
int main()<br>Widget a("Button A");<br>a.click();<br>a.click();
std::println("clicks {}", a.clickCount()); // prints "clicks 2"
Widget b = a;<br>b.click();
std::println("clicks {}", b.clickCount()); // prints "clicks 3"
A problem that may seem subtle is that constness is not propagated from Widget to Impl. In a const method, such as clickCount() above, we can change the state because even if the Impl pointer is const, the object it points to is not. Therefore, the following would compile and produce unexpected results:
int Widget::clickCount() const { return ++pimpl_->clicks; }
On the other hand, a consequence of the move semantics is that a moved-from Widget object has a null pimpl_ pointer and calling any function that use the pointer is undefined behavior (UB).
Widget a("Button A");<br>Widget b = std::move(a);<br>a.click(); // undefined behavior
There are several ways to address this:
document that moved-from objects cannot be used
add a check that pimpl_ is not null everywhere before its usage
provide a function that indicates whether the object is in a valid state so clients can query whether they can use the widget or not
Implementing using std::unique_ptr
We can simplify the implementation by using the C++11 std::unique_ptr type instead of a raw pointer. This automatically manages the allocated object so we don’t have to do resource management explicitly.
#pragma once
#include<br>#include
class Widget<br>public:<br>Widget(const std::string& name);<br>~Widget();<br>Widget(Widget&&) noexcept;<br>Widget& operator=(Widget&&) noexcept;
Widget(const Widget& other);<br>Widget& operator=(const Widget& other);
void click();<br>int clickCount() const;<br>std::string label() const;
private:<br>struct Impl;<br>std::unique_ptr pimpl_;<br>};
#include...