The pimpl idiom is a common approach to hiding implementation details in C/C++. A typical C++ use case looks like:
// .h
class FileImpl;
class File
{
File();
~File();
bool Open(const char* path);
void Close();
std::unique_ptr<FileImpl> pimpl;
};
There are some problems with this approach:
What if we did it more C-style? Still has similar problems, but at least there isn’t the OOP trap to fall into.
// .h
struct file_impl_t;
struct file_t
{
struct file_impl_t* pimpl;
};
bool file_open(struct file_t* file, const char* path);
void file_close(struct file_t* file);
Since we're in C, we don't have any special requirements regarding RAII. What if we reserved all the space for the private implementation inside the struct?
// .h
struct file_t
{
char pimpl[32];
};
// .c
struct file_impl_t
{
// OS-specific data
};;
_Static_assert(sizeof(struct file_impl_t) <= sizeof(struct file_t));
No extra dynamic allocation or pointer hop needed. All this takes is an extra cast inside the functions. Both C++11 and C11 have static assert, so you can guarantee the casts are safe and catch potential cast size mismatches at build time.
bool file_open(struct file_t* file, const char* path)
{
struct file_impl_t* impl = (struct file_impl_t*)file;
// ...
}
This also folds nicely into the principle of zero-by-default. If the API follows this convention, users can simply zero-initialize the struct and not have to worry about another function that initializes the private implementation. This approach works well with C-style codebases that don’t do a lot of RAII.
The main drawback of this approach is the types become annoying to inspect in a debugger, since you have to cast them to the pimpl type. Most debuggers have custom visualizers that will make this a non-issue, like Visual Studio and gdb.
Here's an example for the above code in Visual Studio's visualizer:
<?xml version="1.0" encoding="utf-8">
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010">
<Type Name="file_t">
<Expand>
<Item Name="Impl">(file_impl_t*)impl</Item>
</Expand>
</Type>
</AutoVisualizer>
I call this technique "flat pimpl" because it's essentially flattening the old pointer pimpl into the parent struct. That's pretty much it. It's a pretty simple idea, but easy to remember.