short

std::indirect makes PImpl copyable without writing a copy constructor

· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak

The PImpl idiom hides a class’s implementation behind a pointer so that changing the implementation does not force every translation unit that includes the header to recompile. The standard modern spelling uses std::unique_ptr<Impl>, and it works, at the cost of three irritations that have nothing to do with the idea:

  • unique_ptr is move-only, so a copyable class needs a hand-written copy constructor and copy assignment that allocate a new Impl and copy it.
  • The destructor must be defined in the translation unit where Impl is complete, so you declare ~Widget(); in the header and Widget::~Widget() = default; in the source, forever.
  • const does not propagate. In a const member function the pointer is const, not the pointee, so a const method can happily mutate the implementation and the compiler will not object.

C++26 adds the type that removes all three: std::indirect<T> (P3019). It is not a smart pointer. It is an indirect value: the object it refers to is part of the value, so copying copies deeply, const propagates through, and it is never null except in the moved-from state.

The demo copies a Widget, mutates the copy, and shows the original untouched:

a=7 b=99
copyable and movable with nothing hand-written: true

There is no user-declared copy constructor, no copy assignment, no out-of-line destructor. All five special members are implicit, and get() const cannot modify the implementation because the constness reaches through.

The pair, and where the memory lives

std::indirect comes with std::polymorphic<T> (also in GCC 16.1’s libstdc++, __cpp_lib_polymorphic). The split is about what you need:

  • indirect<T> copies as T. Use it when the implementation type is known and fixed, which is the PImpl case.
  • polymorphic<T> copies the dynamic type through a base pointer, so a polymorphic<Shape> holding a Circle copies a Circle. That is the long-standing “value semantics for a class hierarchy” problem, solved without a hand-written clone() on every derived class.

Both allocate, and both are allocator-aware, so a std::pmr arena works underneath them if allocation on the hot path is a concern. Neither gives you a small-object optimisation: indirect is an indirection by design, and if you wanted the object inline you did not want PImpl.

The compile-time firewall behaves exactly as before: Impl stays incomplete in the header and callers still do not recompile when it changes. What goes away is the paperwork that used to come with it.


Sources: P3019 “indirect and polymorphic” · cppreference: std::indirect · Marius Bancila, “The PImpl idiom and the C++26 std::indirect type”.