• 05/28/16 04:38 AM
    Sign in to follow this  

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

    General and Gameplay Programming

    Klutzershy

    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 [tt]#include[/tt] 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: [code] // Foo.hpp #include class Foo { public: Foo(int); private: // how embarrassing! Dongle dongle; }; // Foo.cpp Foo(int bar) : dongle(bar) {} [/code] Now, it's developed a bad case of PIMPLs and decides to cover up: [code] // 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 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 } [/code] 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. [subheading]Pop that PIMPL![/subheading] 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: [tt]std::aligned_storage[/tt] in the [tt][/tt] 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! [subheading]Implementation[/subheading] 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 [tt]Impl[/tt] 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 [tt]aligned_storage[/tt]. [code] #include template struct Pimpl { typename std::aligned_storage::type mem; }; [/code] For convenience, we'll override the dereference operators to make it look almost like we're directly using the [tt]Impl[/tt] structure. [code] Impl &operator *() { return reinterpret_cast(mem); } Impl *operator ->() { return reinterpret_cast(&mem); } // be sure to add const versions as well! [/code] 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: [code] Pimpl() { static_assert(sizeof(Impl) <= Size, "Impl too big!"); static_assert(Align % alignof(Impl) == 0, "Impl misaligned!"); } [/code] You could also add a variadic template constructor that forwards its parameters to the [tt]Impl[/tt] 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! [code] // 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; }; // Foo_NewPIMPL.cpp #include struct Foo::Impl { Dongle dongle; }; Foo(int bar) { impl->dongle = Dongle{bar}; } [/code] [subheading]Conclusion[/subheading] There's not much to say about it, really. Aside from the [tt]reinterpret_cast[/tt]s, 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!



      Report Article
    Sign in to follow this  


    User Feedback

    Create an account or sign in to leave a review

    You need to be a member in order to leave a review

    Create an account

    Sign up for a new account in our community. It's easy!

    Register a new account

    Sign in

    Already have an account? Sign in here.

    Sign In Now


    IYP

    Report ·

      

    Share this review


    Link to review
    Aardvajk

    Report ·

      

    Share this review


    Link to review
    Krypt0n

    Report ·

      

    Share this review


    Link to review
    Krohm

    Report ·

      

    Share this review


    Link to review
    hanhau

    Report ·

      

    Share this review


    Link to review