The PImpl Idiom: A Quick Refresher
The PImpl (Pointer to Implementation) idiom hides a class's implementation details behind an opaque pointer. It reduces compile-time dependencies and improves encapsulation. Traditionally, C++ developers implemented it with raw pointers, following the Rule of Five, or with std::unique_ptr to automate memory management. However, both approaches have shortcomings: raw pointers require manual memory management and const correctness is not propagated; std::unique_ptr improves safety but still lacks const propagation and can be null after a move.
C++26 Introduces std::indirect
C++26, via paper P3019R14, adds std::indirect (and its polymorphic counterpart std::polymorphic) to the `` header. This vocabulary type is designed for class members that are dynamically allocated but should behave like values. It addresses three key issues:
- Deep copy: Copying an
std::indirectcopies the ownedTobject. - Const propagation: Through a const
std::indirect, you get only const access to the owned object. - Never null: It always holds a value, except in the moved-from state, which can be checked via
valueless_after_move().
Code Comparison: Raw Pointer vs. unique_ptr vs. indirect
Raw Pointer PImpl
// Widget.h
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_;
};
Manual new/delete in each special member function. Constness is not propagated: a const method can modify the pointed-to Impl.
unique_ptr PImpl
// Widget.h
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 pimpl_;
};
Destructor, move constructor/assignment can be = default in the .cpp file (where Impl is complete). Copy constructor/assignment must be user-defined. Constness still not propagated, and moved-from unique_ptr is null.
std::indirect PImpl (C++26)
// Widget.h
#include
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 pimpl_;
};
All special member functions can be = default in the .cpp file. std::indirect handles deep copy automatically, propagates constness, and ensures the pointer is never null (except after move). The valueless_after_move() method replaces null checks.
Key Benefits of std::indirect
- Const propagation: In a
constmember function,pimpl_->clicksisconst int&, preventing accidental modification. This catches bugs at compile time. - Deep copy by default: The compiler-generated copy constructor for
Widgetnow works correctly, copying theImplobject. - No null state: Except after a move,
std::indirectalways contains a value. This eliminates a whole class of undefined behavior from null pointer dereferences. - Simplified code: No manual
new/delete, no user-defined copy constructors for deep copy, noif (this != &other)boilerplate.
Caveats and Usage Notes
std::indirectstill requires the special member functions to be declared in the header and defined in the .cpp file (whereImplis complete), similar tostd::unique_ptr.- The
valueless_after_move()method can be used to check the state. For example:
void Widget::click() {
assert(!pimpl_.valueless_after_move() && "use of moved-from Widget");
++pimpl_->clicks;
}
- As of writing, only GCC 16 supports
std::indirect. Other compilers are expected to follow as C++26 is ratified.
Why This Matters
PImpl is widely used in large C++ codebases to reduce build times and hide implementation. std::indirect eliminates the boilerplate and common pitfalls (constness, null state) that have plagued PImpl implementations for decades. It's a direct improvement to daily developer experience.
Editor's Take
I've been writing C++ for over a decade, and I've seen countless PImpl bugs in code reviews—forgotten copy constructors, const methods that mutate state, and null pointer crashes after moves. std::indirect feels like a long-overdue standard library addition that finally makes PImpl safe by default. I plan to adopt it as soon as GCC 16 is stable in our CI pipeline. My only hesitation: the ecosystem will take time to catch up, so for now, I'll keep a fallback unique_ptr implementation for older compilers.



