Jump to content
  • Advertisement
  • 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 #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 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 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. [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: std::aligned_storage in the 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 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 template struct Pimpl { typename std::aligned_storage::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(mem); } Impl *operator ->() { return reinterpret_cast(&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; }; // Foo_NewPIMPL.cpp #include struct Foo::Impl { Dongle dongle; }; Foo(int bar) { impl->dongle = Dongle{bar}; } [subheading]Conclusion[/subheading] 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!



      Report Article
    Sign in to follow this  


    User Feedback


    An interesting idea!

     

    Do you have a way to deal with a 'Dongle' that has a non-trivial copy constructor? I'd imagine that with the code above, the Foo::Impl::mem field would be copied in a memcpy() / std::copy()-like manner without invoking the copy-constructor.

     

    The solutions I can think of would be to implement the Impl class' copy constructor & assignment operator create new Impl instances instead or to add a memoryWipe() method to my classes that drops file handles, refcounted resources, etc. without properly destroying them. Both are ugly :)

    Share this comment


    Link to comment
    Share on other sites

    This is only a partial solution.

     

    The main reason why people usually hide their class implementation, so they can freely change the memory layout and the contained data structures in the class without breaking an API. Your solution does not help, if the class needs more meory than the predefined amount , so in this regard this is barely considered an improvement over not hiding the variables.

     

    If the class is not part of any public API, there is no reason to hide the parameters, and also this code is not considered to be boilerplate.

     

    +what cygon said.

    Share this comment


    Link to comment
    Share on other sites
    Yeah, I'm not a fan of this. This is the kind of code that programmers write thinking that they're being clever but are really just writing obtuse, error-prone code.

    If your big performance bottleneck worth solving is dynamic allocation of Pimpl classes, then either you're _massively_ overusing Pimpl, you're imagining performance bottlenecks without profiling first and are rushing off to find clever solutions to phantom problems, or you have written the most wonderfully efficient engine the world has ever seen so you're just out of more important issues to address. I confidently assume this case is one of the first two.

    Use your C++ prowess to solve real problems. :)

    Share this comment


    Link to comment
    Share on other sites

    Do you have a way to deal with a 'Dongle' that has a non-trivial copy constructor? 

     

    IMO move constructor should do the trick. As in - you're creating an object on-stack, then construct Foo<> (wherever you want), which calls move-constructor from your on-stack object within. 

    Share this comment


    Link to comment
    Share on other sites
    I've just changed my pimpls to this, I've been using "Fast impl" which follows the same paradigm, but had no alignment etc. checks.
     
    one slight improvement I'd suggest is to rather use an inplacement new in the ctor, alike:
     
    Foo(int bar) {
        new ( &impl->dongle ) Dongle(bar);
    }
    
    edit:
    one missing point is, that you actually also have to explicitly call the dtor of the Impl. While it's natural when you use some autoptr implementation, in this case, you actually have to do it manually.

    Share this comment


    Link to comment
    Share on other sites

    If your big performance bottleneck worth solving is dynamic allocation of Pimpl classes, then either you're _massively_ overusing Pimpl, you're imagining performance bottlenecks without profiling first and are rushing off to find clever solutions to phantom problems, or you have written the most wonderfully efficient engine the world has ever seen so you're just out of more important issues to address. I confidently assume this case is one of the first two.


    Or possibly OP is working in an environment where dynamic allocation is disallowed. I worked on at least one recent console game where we weren't supposed to make heap allocations in gameplay code - no new, no std::function, no standard containers apart from std::array - not just because of performance concerns, but because a lot of the gameplay code ran in parallel and we really didn't want fragmentation. It's a very console/embedded programming mindset.

    Faced with such a requirement AND the desire to cut down on compile times by not forcing everything to rebuild when you change the layout of an important structure (important when your compile times are measured in tens of minutes), this doesn't seem like an unreasonable solution.

    Share this comment


    Link to comment
    Share on other sites
    Interesting. I agree with the arguments against this approach posted above, but under certain specific circumstances, this would have been useful.

    I did something similar in the API for my scripting engine. An Om::Value needed to store a type, a pointer to the internal engine state and a four-byte data area. Rather than expose the internal state class to the API I used a void pointer in the Om::Value header as I didn't want the value to require memory allocation as per normal PIMPL. I then had to cast this internally.

    This trick would have been a cleaner way to achieve this I guess.

    Share this comment


    Link to comment
    Share on other sites

    Maybe I'm just not getting this, but...

     

    In order for those static assertions in the Pimpl template (which is a template, and thus must be fully defined in the header that the to-be-pimpled class includes) to evaluate sizeof(Impl), the compiler must already know Impl as a complete type, no?

     

    So, if this is to compile alltogether, it has to include exactly the same amount of headers as before? In fact, the same amount, plus one...

     

    Or am I fundamentally mistaken on something there?

    Share this comment


    Link to comment
    Share on other sites

    Yep, tested that, and it seems I'm right. Sadly, that makes the whole thing pretty useless for the reason stated.
     
    The static_asserts cause a compiler error, and the variadic template constructor that forwards arguments which you left as exercise to the reader (I implemented it for completeness) fails to compile for the exact same reason.
     

    GCC 6.1 says: 

    pimpl.h: In instantiation of 'struct Pimpl<FooImpl, 16ull, 4ull>':
    foo.h:10:23:   required from here
    pimpl.h:12:23: error: invalid application of 'sizeof' to incomplete type 'FooImpl'
       static_assert(sizeof(Impl) <= Size, "Impl too big!");
                           ^
    pimpl.h: In instantiation of 'Pimpl<Impl, Size, Align>::Pimpl(Args&& ...) [with Args = {int&}; Impl = FooImpl; long long unsigned int Size = 16ull; long long unsigned int Align = 4ull]':
    foo.h:14:23:   required from here
    pimpl.h:20:4: error: invalid use of incomplete type 'struct FooImpl'
        new (&storage) Impl(std::forward<Args>(args)...);
        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

     
    Unless, of course, you include the full definition of the implementation, which however renders the whole thing pointless because that's just what you wanted to avoid.
     
    Hmm... I'd attach the complete minimum compileable example, but cannot see a way to attach a zip file on here, is there one?

    Share this comment


    Link to comment
    Share on other sites


    Create an account or sign in to comment

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

    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

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!