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::indirect copies the owned T object.
  • 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

  1. Const propagation: In a const member function, pimpl_->clicks is const int&, preventing accidental modification. This catches bugs at compile time.
  2. Deep copy by default: The compiler-generated copy constructor for Widget now works correctly, copying the Impl object.
  3. No null state: Except after a move, std::indirect always contains a value. This eliminates a whole class of undefined behavior from null pointer dereferences.
  4. Simplified code: No manual new/delete, no user-defined copy constructors for deep copy, no if (this != &other) boilerplate.

Caveats and Usage Notes

  • std::indirect still requires the special member functions to be declared in the header and defined in the .cpp file (where Impl is complete), similar to std::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.