Type-safe Pimpl implementation without overhead

by Malte Skarupke

I like the pimpl idiom because I like to keep my headers as clean as possible, and other people’s headers are dirty. Unfortunately the pimpl idiom never feels like a good solution because it has runtime overhead that wouldn’t be needed if I didn’t care about clean headers so much.

If you’re not familiar with the Pimpl idiom, it stands for “pointer to implementation” and you use it in C/C++ headers to use a class without having to include the other header in your header. You can also use it to hide your implementation from your users so that you can change the internals of your class and nobody has to know. It’s used all over the place but it has one disadvantage: You always need an extra heap allocation and every method performs an extra pointer dereference.

This code fixes that, so that there can be zero runtime overhead. Here’s how to use it:

class btRigidBody;
class MyRigidBody
{
    // ...
    ForwardDeclaredStorage<btRigidBody, 768> bulletBody;
};

And the code is below:

#pragma once

#include <utility>

namespace detail
{
template<size_t ExpectedSize, size_t ActualSize, size_t ExpectedAlignment, size_t ActualAlignment>
inline void compare_size()
{
    static_assert(ExpectedSize == ActualSize, &quot;The size for the ForwardDeclaredStrage is wrong&quot;);
    static_assert(ExpectedAlignment == ActualAlignment, &quot;The alignment for the ForwardDeclaredStrage is wrong&quot;);
}
template<size_t ExpectedSize, size_t ActualSize, size_t ExpectedAlignment, size_t ActualAlignment>
struct size_comparer
{
    inline size_comparer()
    {
        // going through one additional layer to get good error messages
        // if I put the assert down one more template layer, gcc will show the
        // sizes in the error message
        compare_size<ExpectedSize, ActualSize, ExpectedAlignment, ActualAlignment>();
    }
};
}

struct forwarding_constructor {};

template<typename T, size_t Size, size_t Alignment = 16>
struct ForwardDeclaredStorage
{
    ForwardDeclaredStorage()
    {
        new (&Get()) T();
    }
    template<typename... Args>
    ForwardDeclaredStorage(forwarding_constructor, Args &&... args)
    {
        new (&Get()) T(std::forward<Args>(args)...);
    }
    ForwardDeclaredStorage(const ForwardDeclaredStorage & other)
    {
        new (&Get()) T(other.Get());
    }
    ForwardDeclaredStorage(const T & other)
    {
        new (&Get()) T(other);
    }
    ForwardDeclaredStorage(ForwardDeclaredStorage && other)
    {
        new (&Get()) T(std::move(other.Get()));
    }
    ForwardDeclaredStorage(T && other)
    {
        new (&Get()) T(std::move(other));
    }
    ForwardDeclaredStorage & operator=(const ForwardDeclaredStorage & other)
    {
        Get() = other.Get();
        return *this;
    }
    ForwardDeclaredStorage & operator=(const T & other)
    {
        Get() = other;
        return *this;
    }
    ForwardDeclaredStorage & operator=(ForwardDeclaredStorage && other)
    {
        Get() = std::move(other.Get());
        return *this;
    }
    ForwardDeclaredStorage & operator=(T && other)
    {
        Get() = std::move(other);
        return *this;
    }
    ~ForwardDeclaredStorage()
    {
        detail::size_comparer<Size, sizeof(T), Alignment, alignof(T)> compare_size{};
        Get().~T();
    }
    T & Get()
    {
        return reinterpret_cast<T &>(*this);
    }
    const T & Get() const
    {
        return reinterpret_cast<const T &>(*this);
    }

private:
    __attribute__((aligned(Alignment))) unsigned char storage[Size];
};

This uses a well-known hack where you put the necessary storage into your class, and then placement-new the forward declared object into the storage. But the benefit of this template is that it’s all type-safe and the default copy/move constructor, destructor and assignment operators all do the right thing.

I use the forwarding_constructor struct as a required argument for the forwarding constructor, because constructors with perfect forwarding can otherwise mess up overload resolution. You use it like this:

ForwardDeclaredStorage<widget> a(forwarding_constructor{}, args, for, widget);

The downside of the ForwardDeclaredStorage compared to regular pimpl is that you have to keep the size given in the template in sync with the struct that you’re forward-declaring. So a change in the implementation can still cause a recompilation of all users. In my case that doesn’t matter because I use this to hide the libraries that I’m using, and the size of their structs only change when I update the library version. And there’s a static_assert there to prevent meĀ  from getting the size or the alignment wrong. (Funny thing: I only added the assert for alignment because I felt bad about publishing this when I was only checking the size. Turns out that the code example from the beginning of the post is actually incorrect, because the btRigidBody class is 64 byte aligned for reasons unknown and unenforced)

The license for this code is this:

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>