Flat Pimpl

2020-7-19

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.