The PImpl Idiom: A Classic Solution

For decades, C++ developers have relied on the PImpl (Pointer to Implementation) idiom to manage class interfaces, decouple implementation details from the public API, and enable binary compatibility. This technique involves hiding the private data members and implementation details of a class within a separate, private implementation class. The public class then holds a pointer to an instance of this implementation class. This separation provides several key benefits:

  • Reduced Compile Times: Changes to the private implementation do not require recompilation of all classes that use the public interface.
  • Encapsulation: It enforces a strict separation between interface and implementation, preventing users from accessing or depending on private details.
  • Binary Compatibility: It allows for changes to the implementation without breaking existing compiled code, provided the public interface remains stable.

The traditional PImpl idiom often involves manual memory management (e.g., using new and delete) or smart pointers like std::unique_ptr. While effective, it adds boilerplate code and can be prone to errors if not handled meticulously. The implementation class is typically defined in a separate `.cpp` file, ensuring that its details are never exposed in the header.

Consider a `Widget` class. The header file might declare:

// Widget.h
class Widget {
public:
    Widget();
    ~Widget();
    void doSomething();
private:
    struct Impl;
    std::unique_ptr<Impl> pimpl;
};

And the source file would define the Impl struct and implement the methods:

// Widget.cpp
struct Widget::Impl {
    // private data and methods
    void doSomethingImpl();
};

Widget::Widget() : pimpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;
void Widget::doSomething() { pimpl->doSomethingImpl(); }

This pattern, while robust, adds verbosity and indirection. The indirection is necessary for type erasure – hiding the concrete type of the implementation. This is particularly useful for classes that manage resources or have complex internal states that should not be exposed.

Introducing std::indirect in C++26

C++26 introduces std::indirect<T>, a new utility designed to simplify and enhance type erasure and value semantics. It acts as a type-erasing wrapper that stores an object of a type T (or a type convertible to T) and provides a uniform interface to interact with it, regardless of its underlying concrete type. This makes it a powerful candidate for modernizing the PImpl idiom and other scenarios requiring dynamic polymorphism without explicit virtual functions.

std::indirect<T> essentially encapsulates a value of a type U, where U satisfies the requirements of T. It offers value semantics, meaning it can be copied, moved, and assigned, and it manages its own storage. This is a significant departure from raw pointers or even smart pointers that might require more manual setup for full value semantics.

The primary advantage of std::indirect is its ability to abstract away the concrete type. It allows you to hold a value of any type that conforms to a specific interface (defined by T) without knowing that concrete type at compile time. This is the essence of type erasure.

std::indirect as a PImpl Replacement

The PImpl idiom's core purpose is to hide implementation details and enable type erasure. std::indirect directly addresses this by providing a built-in mechanism for type erasure with value semantics. Instead of manually managing a pointer to an implementation class, a developer can use std::indirect to hold the implementation details.

Consider the previous `Widget` example rewritten using std::indirect. If we define an abstract interface or a concrete implementation type that std::indirect can wrap, the header file could become significantly cleaner:

// Widget.h (using std::indirect)
#include <indirect>

class Widget {
public:
    Widget();
    ~Widget();
    void doSomething();

private:
    // std::indirect will hold the actual implementation details
    // For example, if Impl is the concrete type:
    std::indirect<ImplInterface> pimpl;
};

Here, ImplInterface would be the type std::indirect is configured to manage. This could be an abstract base class with virtual functions, or a concrete type if std::indirect is specialized to handle it directly. The key is that the header no longer exposes the Impl struct or any pointer management.

The `std::indirect` type handles the storage and dispatch of operations to the underlying implementation. This means the boilerplate code for managing the implementation object is drastically reduced. The storage for the implementation can even be in-place if the size is known and small enough, avoiding heap allocations entirely in some cases, which is a significant performance optimization over traditional PImpl with std::unique_ptr.

Benefits and Use Cases

The introduction of std::indirect offers several compelling benefits:

  • Simplified Syntax: Less manual boilerplate for PImpl and type-erased objects.
  • Value Semantics: Objects of std::indirect can be copied and moved, simplifying their use in containers and as class members.
  • Potential for In-Place Storage: Can avoid dynamic allocations, improving performance and reducing memory fragmentation.
  • Modernizing Libraries: Provides a standard, idiomatic way to achieve type erasure, making libraries more robust and easier to maintain.
  • Alternative to Polymorphic Wrappers: Offers a more efficient alternative to patterns like the