You actually don't need to do that at all. It's common style in C++, but the language does not require it.
With the right techniques, you can absolutely forward-declare basically all of a class's functionality. Then you can put it into its own translation unit.
Members and function signatures have to be declared in the header, but details about member values/initialization and function implementations can absolutely be placed in a single translation unit.
The data layout of a class must be part of its definition. So either you expose the layout (data members), add a layer of indirection with PIMPL, or resort to ugly hacks to otherwise hide the data layout such as having buffers as members. Another possibility is to not use exposed classes for member functions. Then you can just pass pointers around only and never use C++ features. Out of all of these, PIMPL solves the problem the best.
Yes, that's true. But if the concern is build-times, exposing the data layout is harmless. We don't necessarily need full PIMPL just to get improved build times. By keeping the data layout in the .hpp, you can guarantee that your class can still stack-allocate.
if the concern is build-times, exposing the data layout is harmless
Not at all! If your private member variables use any interesting types (and why shouldn't they?) you need to include all the headers for those types too. That's exactly why you get an explosion of header inclusion.
If you change the data layout of your exposed class, you must recompile anything that uses it. That increases build times and also breaks ABI. And as the other guy commented, the data itself has types that also need definitions and file inclusions. Without PIMPL, your data layout can change without touching your header, due to changes in a related header (even a 3rd party header).
With the right techniques, you can absolutely forward-declare basically all of a class's functionality. Then you can put it into its own translation unit.
Members and function signatures have to be declared in the header, but details about member values/initialization and function implementations can absolutely be placed in a single translation unit.