The pimpl idiom (pointer to implementation) hides a class's private data members behind a single opaque pointer to a forward-declared implementation struct. The header that clients #include only ever sees the pointer and a forward declaration, never the actual member types, which means changing the private implementation no longer forces every translation unit that includes the header to recompile.
// widget.h — clients only see this class Widget { public: Widget(); ~Widget(); void doThing(); private: struct Impl; std::unique_ptr<Impl> impl; };
// widget.cpp — implementation detail, changes here don't touch widget.h struct Widget::Impl { int state = 0; std::vector<double> data; }; Widget::Widget() : impl(std::make_unique<Impl>()) {} Widget::~Widget() = default; void Widget::doThing() { impl->state++; }
std::unique_ptr<Impl>'s destructor needs Impl's full definition to call delete on it, and at the point the compiler processes widget.h, Impl is only forward-declared. If the destructor is left implicit (or defined inline in the header), the compiler tries to instantiate unique_ptr<Impl>'s deleter before Impl is a complete type, and the build fails with an error about deleting an incomplete type. Declaring ~Widget() in the header but defining it (even as = default) in the .cpp file, after Impl is fully defined, defers that instantiation to where it can succeed. The same reasoning forces the constructor, or anything else that touches impl, out of the header too, which is why pimpl classes end up with more out-of-line boilerplate than a typical class.
Compile-time isolation is the main win: a header using pimpl only changes (forcing dependent recompiles) when the public interface changes, not when a private member is added or a private dependency's header changes. This matters most in large codebases where a widely-included header pulling in heavy dependencies (say, a networking library used only internally) forces a full rebuild on every change to that dependency. Pimpl also gives a clean ABI boundary: the size and layout of Widget as seen by client code stays stable even if Impl changes size, which is useful for shared libraries that need binary compatibility across versions.
Every call through a pimpl'd class adds a pointer indirection and, if Impl is heap-allocated (the normal case), a heap allocation per object plus a cache miss on the indirection. This is a real cost in hot paths, and the extra out-of-line boilerplate (constructor, destructor, and any special member function that would otherwise be implicitly generated) means pimpl is worth reaching for only when compile-time isolation or ABI stability actually matters, not as a default way to write classes.