std::indirect makes PImpl copyable without writing a copy constructor
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_ptris move-only, so a copyable class needs a hand-written copy constructor and copy assignment that allocate a newImpland copy it.- The destructor must be defined in the translation unit where
Implis complete, so you declare~Widget();in the header andWidget::~Widget() = default;in the source, forever. constdoes not propagate. In aconstmember function the pointer is const, not the pointee, so aconstmethod 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 asT. 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 apolymorphic<Shape>holding aCirclecopies aCircle. That is the long-standing “value semantics for a class hierarchy” problem, solved without a hand-writtenclone()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”.