The PImpl idiom and the C++26 std::indirect type

Lobsters Hottest Tools

Summary

Explains the PImpl idiom in C++ and how the upcoming C++26 std::indirect type simplifies its implementation, reducing boilerplate and manual memory management.

<p><a href="https://lobste.rs/s/gtxmc4/pimpl_idiom_c_26_std_indirect_type">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 07/24/26, 04:58 AM

# The PImpl idiom and the C++26 std::indirect type Source: [https://mariusbancila.ro/blog/2026/07/23/the-pimpl-idiom-and-the-cpp26-stdindirect-type/](https://mariusbancila.ro/blog/2026/07/23/the-pimpl-idiom-and-the-cpp26-stdindirect-type/) 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 <string> class Widget { public: Widget(const std::string& name); ~Widget(); Widget(const Widget& other); Widget& operator=(const Widget& other); Widget(Widget&& other) noexcept; Widget& operator=(Widget&& other) noexcept; void click(); int clickCount() const; std::string label() const; private: struct Impl; Impl* pimpl_; }; ``` 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 { std::string name; int clicks = 0; explicit Impl(std::string n) : name(std::move(n)) {} }; Widget::Widget(const std::string& name) : pimpl_(new Impl(name)) { } Widget::~Widget() { delete pimpl_; } Widget::Widget(const Widget& other) : pimpl_(new Impl(*other.pimpl_)) { } Widget& Widget::operator=(const Widget& other) { if (this != &other) { Impl* tmp = new Impl(*other.pimpl_); delete pimpl_; pimpl_ = tmp; } return *this; } Widget::Widget(Widget&& other) noexcept : pimpl_(other.pimpl_) { other.pimpl_ = nullptr; } Widget& Widget::operator=(Widget&& other) noexcept { if (this != &other) { delete pimpl_; pimpl_ = other.pimpl_; other.pimpl_ = nullptr; } return *this; } void Widget::click() { ++pimpl_->clicks; } int Widget::clickCount() const { return pimpl_->clicks; } 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<Widget\>`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 <iostream> #include <print> #include "Widget.h" int main() { Widget a("Button A"); a.click(); a.click(); std::println("clicks {}", a.clickCount()); // prints "clicks 2" Widget b = a; 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"); Widget b = std::move(a); 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 <memory> #include <string> class Widget { public: Widget(const std::string& name); ~Widget(); Widget(Widget&&) noexcept; Widget& operator=(Widget&&) noexcept; Widget(const Widget& other); Widget& operator=(const Widget& other); void click(); int clickCount() const; std::string label() const; private: struct Impl; std::unique_ptr<Impl> pimpl_; }; ``` ``` #include "Widget.h" struct Widget::Impl { std::string name; int clicks = 0; explicit Impl(std::string n) : name(std::move(n)) {} }; Widget::Widget(const std::string& name) : pimpl_(std::make_unique<Impl>(name)) { } 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) { if (this != &other) { *pimpl_ = *other.pimpl_; } return *this; } void Widget::click() { ++pimpl_->clicks; } int Widget::clickCount() const { return pimpl_->clicks; } std::string Widget::label() const { return pimpl_->name; } ``` In this implementation, the copy constructor and copy assignment are explicitly user\-defined\. The destructor, move constructor, and move assignment operator are defaulted to the compiler but this must happen in the \.cpp file because in the header`Widget::Impl`is an incomplete type and the`unique\_ptr`‘s deleter needs to know the size of Impl\. Although there is no manual handling of resources anymore, the constness issue remains the same as well as the null`pimpl\_`object after a move\. ## Implementing using std::indirect This is where the`std::indirect`enters the scene\. This is a new vocabulary type from C\+\+26, defined in the`<memory\>`header\. It is intended to be used for class members that are dynamically allocated but need to behave like values\. It’s designed to be used instead of`std::unique\_ptr`where its semantics \(not copyable, does not propagate const, and can be`null`\) are not appropriate\. The perfect example for this is Pimpl\. The following snippet shows the`Widget`class implemented using`std::indirect`: ``` #pragma once #include <memory> #include <string> class Widget { public: Widget(const std::string& name); Widget(const Widget&); Widget(Widget&&) noexcept; Widget& operator=(const Widget&); Widget& operator=(Widget&&) noexcept; ~Widget(); void click(); int clickCount() const; std::string label() const; private: struct Impl; std::indirect<Impl> pimpl_; }; ``` ``` #include "widget.h" struct Widget::Impl { std::string name; int clicks = 0; explicit Impl(std::string n) : name(std::move(n)) {} }; Widget::Widget(const std::string& name) : pimpl_(std::in_place, name) {} Widget::Widget(const Widget&) = default; Widget::Widget(Widget&&) noexcept = default; Widget& Widget::operator=(const Widget&) = default; Widget& Widget::operator=(Widget&&) noexcept = default; Widget::~Widget() = default; void Widget::click() { ++pimpl_->clicks; } int Widget::clickCount() const { return pimpl_->clicks; } std::string Widget::label() const { return pimpl_->name; } ``` If we compare these implementation, we’ll see that the five special member functions are still declared in the header but they are defaulted \(for compiler implementation\) in the source file, where the`Impl`type is complete and known to the compiler \(this is the same requirement as for`std::unique\_ptr`\)\. The`std::indirect`type owns a single`T`object allocated on the heap, but behaves like a`T`member: - **Deep copy**: copying the`std::indirect<T\>`object copies the owned object, so your class’s compiler\-generated copy constructor just works\. - **Const propagation**: through a`const std::indirect<T\>`object, you only get const access to the`T`\. - **Never null**: it always holds a value, except in the moved\-from state \(but features a member function called`valueless\_after\_move\(\)`that lets you check the state\)\. Using`std::indirect`does not prevent us from having to explicitly declare and default the special member functions but does help us with propagating constness, which means the code is more robust towards accidental changes\. The`valueless\_after\_move\(\)`is basically a replacement for null check from`unique\_ptr`\. For instance, we can add an assert to all the functions that use the`pimpl\_`object to ensure they are not invoked after the widget has been moved\. ``` void Widget::click() { assert(!pimpl_.valueless_after_move() && "use of moved-from Widget"); ++pimpl_->clicks; } ``` Another example of using`valueless\_after\_move\(\)`is erase\-remove from a vector from which we moved out objects\. Here is an example: ``` std::vector<std::indirect<Widget>> widgets = ...; std::vector<std::indirect<Widget>> selected; for (auto& w : widgets) if (select(w)) selected.push_back(std::move(w)); // leaves a valueless widget behind std::erase_if(widgets, [](const auto& w) { return w.valueless_after_move(); // remove the dangling object }); ``` This is possible with`std::unique\_ptr`too, but the check would be a`w == nullptr`\. Therefore, it’s just a more verbose way of checking that an`std::indirect`object still holds a value or not\. `std::indirect`comes with a companion,`std::polymorphic`, but we will look at that in another article\. At the time of writing this, only GCC 16 supports`std::indirect`\. ## See also - [PImpl](https://en.cppreference.com/cpp/language/pimpl) - [Pimpl For Compile\-Time Encapsulation \(Modern C\+\+\)](https://learn.microsoft.com/en-us/cpp/cpp/pimpl-for-compile-time-encapsulation-modern-cpp?view=msvc-170) - [Modern C\+\+: The Pimpl Idiom](https://medium.com/@weidagang/modern-c-the-pimpl-idiom-53173b16a60a) - [P3019R14: indirect and polymorphic: Vocabulary Types for Composite Class Design](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3019r14.pdf)

Similar Articles

C++26: Reducing undefined behaviour

Lobsters Hottest

C++26 introduces changes to reduce undefined behavior, notably making it ill-formed to delete a pointer to an incomplete type, improving program safety.

C++26: More function wrappers

Lobsters Hottest

C++26 introduces two new function wrappers: std::copyable_function, which provides a copyable, const-correct alternative to std::function, and std::function_ref, a non-owning callable reference with reference semantics.

Beautiful Type Erasure with C++26 Reflection

Hacker News Top

Introduces rjk::duck, a C++26 reflection library that simplifies type erasure with a clean interface and minimal boilerplate, leveraging the new C++26 reflection features.

C++26: Standard library hardening

Lobsters Hottest

C++26 is introducing standardized library hardening to catch common undefined behavior (like out-of-bounds access) at runtime, based on Google's production experience showing a mere 0.30% performance overhead and a 30% reduction in segmentation faults.

The Fil-C Optimized Calling Convention

Hacker News Top

The Fil-C optimized calling convention ensures memory safety for C programs even under adversarial misuse, while maintaining efficiency by omitting safety checks in the common case. It explains the generic and register-passing optimizations that handle type violations via panics or well-defined behavior.