Quantcast
Channel: GameDev.net
Viewing all articles
Browse latest Browse all 17825

Static, zero-overhead (probably) PIMPL in C++

$
0
0

PIMPL (Pointer to IMPLementation, or "opaque pointer") is an idiom used for when you need "super" encapsulation of members of a class - you don't have to declare privates, or suffer all of the #include bloat or forward declaration boilerplate entailed, in the class definition. It can also save you some recompilations, and it's useful for dynamic linkage as it doesn't impose a hidden ABI on the client, only the one that is also part of the API.

Typical exhibitionist class:

// Foo.hpp
#include <big_header.hpp>
    
class Foo {
public:
	Foo(int);

private:
	// how embarrassing!
	Dongle dongle;
};

// Foo.cpp
Foo(int bar) : dongle(bar) {}

Now, it's developed a bad case of PIMPLs and decides to cover up:
// Foo_PIMPL.hpp
class Foo {
public:
	// API stays the same...
	Foo(int);

	// with some unfortunate additions...
	~Foo();
	Foo(Foo const&);
	Foo &operator =(Foo const&);

private:
	// but privates are nicely tucked away!
	struct Impl;
	Impl *impl;
};

// Foo_PIMPL.cpp
#include <big_header.hpp>

struct Foo::Impl {
	Dongle dongle;
};

Foo(int bar) {
	impl = new Impl{Dongle{bar}}; // hmm...
}

~Foo() {
	delete impl; // hmm...
}

Foo(Foo const&other) {
	// oh no
}

Foo &operator =(Foo const&other) {
	// I hate everything
}

There are a couple big caveats of PIMPL, and that's of course that you need to do dynamic memory allocation and suffer a level of pointer indirection, plus write a whole bunch of boilerplate! In this article I will propose something similar to PIMPL that does not require this sacrifice, and has (probably) no run time overhead compared to using standard private members.

Pop that PIMPL!



So, what can we do about it?

Let's start by understanding why we need to put private fields in the header in the first place. In C++, every class can be a value type, i.e. allocated on the stack. In order to do this, we need to know its size, so that we can shift the stack pointer by the right amount. Every allocation, not just on the stack, also needs to be aware of possible alignment restrictions. Using an opaque pointer with dynamic allocation solves this problem, because the size and alignment needs of a pointer are well-defined, and only the implementation has to know about the size and alignment needs of the encapsulated fields.

It just so happens that C++ already has a very useful feature to help us out: std::aligned_storage in the <type_traits> STL header. It takes two template parameters - a size and an alignment - and hands you back an unspecified structure that satisfies those requirements. What does this mean for us? Instead of having to dynamically allocate memory for our privates, we can simply alias with a field of this structure, as long as the size and alignment are compatible!

Implementation



To that end, let's design a straightforward structure to handle all of this somewhat automagically. I initially modeled it to be used as a base class, but couldn't get the inheritance of the opaque Impl type to play well. So I'll stick to a compositional approach; the code ended up being cleaner anyways.

First of all, we'll template it over the opaque type, a size and an alignment. The size and alignment will be forwarded directly to an aligned_storage.
#include <type_traits>

template<class Impl, size_t Size, size_t Align>
struct Pimpl {
	typename std::aligned_storage<Size, Align>::type mem;
};

For convenience, we'll override the dereference operators to make it look almost like we're directly using the Impl structure.
Impl &operator *() {
	return reinterpret_cast<Impl &>(mem);
}

Impl *operator ->() {
	return reinterpret_cast<Impl *>(&mem);
}

// be sure to add const versions as well!

The last piece of the puzzle is to ensure that the user of the class actually provides a valid size and alignment, which ends up being quite trivial:
Pimpl() {
	static_assert(sizeof(Impl) <= Size, "Impl too big!");
	static_assert(Align % alignof(Impl) == 0, "Impl misaligned!");
}

You could also add a variadic template constructor that forwards its parameters to the Impl constructor and constructs it in-place, but I'll leave that as an exercise to the reader.

To end off, let's convert our Foo example to our new and improved PIMPL!
// Foo_NewPIMPL.hpp
class Foo {
public:
	// API stays the same...
	Foo(int);
    
	// no boilerplate!

private:
	struct Impl;
	// let's assume a Dongle will always be smaller than 16 bytes and require 4-byte alignment
	Pimpl<Impl, 16, 4> impl;
};

// Foo_NewPIMPL.cpp
#include <big_header.hpp>

struct Foo::Impl {
	Dongle dongle;
};

Foo(int bar) {
	impl->dongle = Dongle{bar};
}

Conclusion



There's not much to say about it, really. Aside from the reinterpret_casts, there's no reason there could be any difference at run time, and even then the only potential difference would be in the compiler's ability to optimize.

As always, I appreciate comments and feedback!

Viewing all articles
Browse latest Browse all 17825

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>