8

One of the issues of pimpl is the performance penalty of using it (additional memory allocation, non-contiguous data members, additional indirections, etc..). I would like to propose a variation on the pimpl idiom that will avoid these performance penalties at the expense of not getting all the benefits of pimpl. The idea is to leave all private data members in the class itself and move only the private methods to the pimpl class. The benefit compared to basic pimpl is that the memory remains contiguous (no additional indirection). The benefits compared to not using pimpl at all are:

  1. It hides the private functions.
  2. You can structure it so that all these functions will have internal linkage and allow the compiler to more aggressively optimize it.

So my idea is to make the pimpl inherit from the class itself (sounds a bit crazy I know, but bear with me). It would look something like this:

In A.h file:

class A
{
    A();
    void DoSomething();
protected:  //All private stuff have to be protected now
    int mData1;
    int mData2;
//Not even a mention of a PImpl in the header file :)
};

In A.cpp file:

#define PCALL (static_cast<PImpl*>(this))

namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
    static_assert(sizeof(PImpl) == sizeof(A), 
                  "Adding data members to PImpl - not allowed!");
    void DoSomething1();
    void DoSomething2();
    //No data members, just functions!
};

void PImpl::DoSomething1()
{
    mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
    DoSomething2();
}

void PImpl::DoSomething2()
{
    mData2 = baz();
}

}
A::A(){}

void A::DoSomething()
{
    mData2 = foo();
    PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}

As far as I see there are absolutely no performance penalties in using this vs no pimpl and some possible performance gains and cleaner header file interface. One disadvantage this has vs standard pimpl is that you can't hide the data members so changes to those data members will still trigger a recompilation of everything that depends on the header file. But the way I see it, it's either get that benefit or the performance benefit of having the members contiguous in memory (or do this hack - "Why Attempt #3 is Deplorable"). Another caveat is that if A is a templated class the syntax gets annoying (you know, you can't use mData1 directly, you need to do this->mData1, and you need to start using the typename and maybe template keywords for dependent types and templated types, etc.). Yet another caveat is that you can no longer use private in the original class, only protected members, so you can't restrict access from any inheriting class, no just the pimpl. I tried but couldn't get around this issue. For example, I tried making the pimpl a friend template class in the hopes of making the friend declaration broad enough to allow me to define the actual pimpl class in an anonymous namespace, but that just doesn't work. If anyone has any idea of how to keep the data members private and still allow an inheriting pimpl class defined in an anonymous namepsace to access those, I'd really like to see it! That would eliminate my main reservation from using this.

I feel though, that these caveats are acceptable for the benefits of what I propose.

I tried looking online for some reference to this "function-only pimpl" idiom but couldn't find anything. I'm really interested in what people think about this. Are there other issues with this or reasons I shouldn't use this?

UPDATE:

I've found this proposal that more or less tries to accomplish exactly what I am, but doing so by changing the standard. I completely agree with that proposal and hope it will make it into the standard (I know nothing of that process so I have no idea of how likely that's to happen). I'd much rather have it possible to do this through a built in language mechanism. The proposal also explains the benefits of what I'm trying to achieve much better than me. It also doesn't have the issue of breaking encapsulation like my suggestion has (private -> protected). Still, until that proposal makes it into the standard (if that ever happens), I think my suggestion makes it possible to get those benefits, with the caveats I listed.

UPDATE2:

One of the answers mentions LTO as a possible alternative to getting some of the benefits (more aggressive optimizations I'm guessing). I'm not really sure exactly what goes on in various compiler optimization passes but I do have a bit of experience with the resulting code (I use gcc). Simply putting the private methods in the original class will force those to have external linkage.

I might be wrong here, but the way I interpret that is that the compile-time optimizer cannot eliminate the function even if all its call instances are completely inlined inside that TU. For some reason even LTO refuses to get rid of the function definition even if it seems that all the call instances in the entire linked binary are all inlined. I found some references stating that it's because the linker doesn't know if you'll somehow still call the function using function pointers (though I don't understand why the linker can't figure out that the address of that method is never taken).

This is not the case if you use my suggestion and put those private methods in a pimpl inside an anonymous namespace. If those get inlined, the functions will NOT appear in (with -O3, that includes -finline-functions) the object file.

The way I understand it, the optimizer, when deciding whether or not to inline a function, takes into account its impact on code size. So, using my suggestion I'm making it slightly "cheaper" for the optimizer to inline those private methods.

  • The use of PCALL is undefined behavior. You cannot cast an A to a PImpl and use it unless the underlying object is actually of type PImpl. However, unless I am mistaken, users will just create objects of type A. – BeeOnRope Oct 11 '17 at 17:28
8

The selling points of the Pimpl pattern are:

  • total encapsulation: there are no (private) data members mentioned in the header file of the interface object.
  • stability: until you break the public interface (which in C++ includes private members), you'll never have to recompile code that depends on the interface object. This makes the Pimpl a great pattern for libraries that don't want their users to recompile all code on every internal change.
  • polymorphism and dependency injection: the implementation or behaviour of the interface object can be easily swapped out at runtime, without requiring dependent code to be recompiled. Great if you need to mock something for an unit test.

To this effect, the classic Pimpl consists of three parts:

  • An interface for the implementation object, which must be public, and use virtual methods for the interface:

    class IFrobnicateImpl
    {
    public:
        virtual int frobnicate(int) const = 0;
    };
    

    This interface is required to be stable.

  • An interface object that proxies to the private implementation. It does not have to use virtual methods. The only allowed member is a pointer to the implementation:

    class Frobnicate
    {
        std::unique_ptr<IFrobnicateImpl> _impl;
    public:
        explicit Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl = nullptr);
        int frobnicate(int x) const { return _impl->frobnicate(x); }
    };
    
    ...
    
    Frobnicate::Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl /* = nullptr */)
    : _impl(std::move(impl))
    {
        if (!_impl)
            _impl = std::make_unique<DefaultImplementation>();
    }
    

    The header file of this class must be stable.

  • At least one implementation

The Pimpl then buys us a great deal of stability for a library class, at the cost of one heap allocation and additional virtual dispatch.

How does your solution measure up?

  • It does away with encapsulation. Since your members are protected, any subclass can mess with them.
  • It does away with interface stability. Whenever you change your data members – and that change is just one refactoring away – you'll have to recompile all dependent code.
  • It does away with the virtual dispatch layer, preventing easy swapping of the implementation.

So for every objective of the Pimpl pattern, you fail to fulfil this objective. It is therefore not reasonable to call your pattern a variation of the Pimpl, it is much more an ordinary class. Actually, it's worse than an ordinary class because your member variables are private. And because of that cast which is a glaring point of fragility.

Note that the Pimpl pattern is not always optimal – there's a tradeoff between stability and polymorphism on the one hand, and memory compactness on the other. It is semantically impossible for a language to have both (without JIT compilation). So if you're micro-optimizing for memory compactness, clearly the Pimpl is not a suitable solution for your use case. You'll also probably stop using half the standard library, since these awful string and vector classes involve dynamic memory allocations ;-)

  • About your last note about using string and vectors - it's very close to the truth :). I understand that pimpl has benefits that my suggestion simply doesn't provide. It does have benefits though, that are more eloquently presented in the link I added in the UPDATE. Considering that performance plays a very big part in my use case, how would you compare my suggestion to not using pimpl at all? Because for my use case it's not my suggestion vs pimpl, it's my suggestion vs no-pimpl at all, since pimpl has performance costs that I don't want to incur. – dcmm88 Aug 7 '15 at 18:57
  • 1
    @dcmm88 If performance is your #1 objective, then obviously things like code quality and encapsulation are fair game. The only thing your solution improves over normal classes is avoiding a recompile when private method signatures change. In my book, that's not terribly much, but if this is a foundational class in a large code base, the weird implementation might be worth it. Consult this handy XKCD chart to determine how much development time you can sink into shortening compile times. Depending on your salary, upgrading your computer might be cheaper. – amon Aug 7 '15 at 19:20
  • 1
    It also allows the private functions to have internal linkage, allowing for more aggressive optimization. – dcmm88 Aug 7 '15 at 19:31
  • 1
    It seems that you expose the public interface of the pimpl class in the header file of the original class. AFAUI the whole idea is to hide as much as possible. Usually the way I see pimpl being implemented, the header file just has a forward declaration of the pimpl class and the cpp has the complete definition of the pimpl class. I think the swapping ability comes with a different design pattern - the bridge pattern. So, I'm not sure it's fair to say my suggestion fails to provide it, when pimpl doesn't usually provide it either. – dcmm88 Aug 7 '15 at 21:44
  • Sorry for last comment. You actually have quite large answer, and the point is at the end, so I missed it – BЈовић Oct 14 '15 at 10:46
3

To me, the advantages don't outweigh the disadvantages.

Advantages:

It can speed up compilation, since it saves a rebuild if only the private method signatures have changed. But rebuilding is necessary if public or protected method signatures or private data members have changed, and it's rare that I have to change private method signatures without touching any of these other options.

It can permit more aggressive compiler optimizations, but LTO should permit many of the same optimizations (at least, I think it can - I'm not a compiler optimization guru), plus some more besides, and can be made standard and automatic.

Disadvantages:

You mentioned a couple of disadvantages: the inability to use private, and complexities with templates. To me, though, the biggest disadvantage is that it's simply awkward: an unconventional programming style, with not-quite-standard pimpl-style jumps between interface and implementation, which will be unfamiliar to any future maintainers or new team members, and which may be poorly supported by tooling (see, e.g., this GDB bug).

The standard concerns about optimization apply here: Have you measured that the optimizations give a meaningful improvement to your performance? Would your performance be better by doing this or by taking the time it would take to maintain this and invest it in profiling hotspots, improving algorithms, etc.? Personally, I'd rather pick a clear and straightforward programming style, on the assumption that it will free up time to do targeted optimizations. But that's my perspective for the kinds of code I work on - for your problem domain, the tradeoffs may be different.

Side note: Permitting private with method-only pimpl

You asked about how to permit private members with your method-only pimpl suggestion. Unfortunately, I consider method-only pimpl to be kind of a hack, but if you've decided that the advantages outweigh the disadvantages, then you might as well embrace the hack.

A.h:

#ifndef A_impl
#define A_impl private
#endif

class A
{
public:
    A();
    void DoSomething();
A_impl:
    int mData1;
    int mData2;
};

A.cpp:

#define A_impl public
#include "A.h"
  • I'm a bit embarrassed to say, but I do like your #define A_impl private suggestion :). I'm not an optimization guru either, but I do have some experience with LTO and fiddling with it was actually what led me to come up with this. I'll update my question with the info I have about using it and why it still doesn't provide the full benefit my method-only pimpl provides. – dcmm88 Aug 7 '15 at 21:54
  • @dcmm88 - Thanks for the info on LTO. It's not something I have a whole lot of experience with. – Josh Kelley Aug 7 '15 at 23:57
1

You could use std::aligned_storage to declare storage for your pimpl at the interface class.

class A
{
std::aligned_storage< 128 > _storage;
public:
  A();
};

In the implementation you can in-place construct your Pimpl class at _storage:

class Pimpl
{
  int _some_data;
};

A::A()
{
  ::new(&_storage) Pimpl();
  // accessing Pimpl: *(Pimpl*)(_storage);
}

A::~A()
{
  ((Pimpl*)(_storage))->~Pimpl(); // calls destructor for inline pimpl
}
  • Forgot to mention you'd need to increase the storage size once in a while, thus forcing recompilation. You will often be able to squeeze many changes that doesn't trigger recompilations - if you give the storage a little slack. – Fabio Oct 14 '15 at 9:59
0

No, you can't implement it without a performance penalty. PIMPL is, by it's very nature, a performance penalty, as you are applying a run-time indirection.

Of course, this depends on exactly what you wanted to indirect. Some information is simply not used by the consumer- like what exactly you intend to put in your 64 4-byte-aligned bytes. But other information is- like the fact that you want 64 4-byte-aligned bytes for your object.

Generic PIMPLs without performance penalties do not exist and will never exist. It's the same information that you deny to your user that they want to use to optimize. If you give it to them, your IMPL is not abstracted; if you deny it to them, they can't optimize. You can't have it both ways.

  • I did provide a possible "partial pimpl" suggestion that I claim has no performance penalty, and possible performance gains. You might resent my calling it anything related to pimpl, but I am hiding some of the implementation detail (the private methods). I could call it private method implementation hiding, but chose to call it a variation of pimpl, or method-only pimpl. – dcmm88 Aug 7 '15 at 22:37
0

With all due respect and not with any intent of killing this excitement, I don't see any practical benefit this serves from a compile-time perspective. A lot of the benefits of pimpls are going to come from hiding user-defined type details. For example:

struct Foo
{
    Bar data;
};

... in such a case, the heaviest cost of compilation comes from the fact that, in order to define Foo, we must know the size/alignment requirements of Bar (which means we must recursively require the definition of Bar).

If you aren't hiding the data members, then one of the most significant benefits from a compile-time perspective is lost. Also there's some potentially dangerous-looking code there but the header isn't getting any lighter and the source file is getting heavier with more forwarding functions, so it's likely to increase, rather than decrease, compilation times overall.

Lighter Headers is the Key

To get a decrease in build times, you want to be able to show a technique that results in a dramatically lighter-weight header (typically by allowing you to no longer recursively #include some other headers because you hid details which no longer require certain struct/class definitions). That's where genuine pimpls can have a significant effect, breaking off cascading chains of header inclusions and yielding far more independent headers with all the private details hidden away.

Safer Ways

If you want to do something like this anyway, it'll also be a whole lot simpler to use a friend defined in your source file rather than one which inherits the same class which you don't actually instantiate with pointer casting tricks to call methods on an uninstantiated object, or simply use free-standing functions with internal linkage inside the source file which receive the appropriate parameters to do the necessary work (either of these could at least allow you to hide some private methods from the header for a very trivial savings in compilation times and a little bit of wiggle room to avoid cascading recompilation).

Fixed Allocator

If you want the cheapest kind of pimpl, the main trick is to use a fixed allocator. Especially when aggregating pimpls in bulk, the biggest killer is the loss of spatial locality and the additional compulsory page faults when accessing the pimpl for the first time. By preallocating memory pools that pool off memory to pimpls being allocated and return the memory to the pool instead of freeing it on deallocation, the cost of a boatload of pimpl instances diminishes dramatically. It's still not free though from a performance standpoint, but a whole lot cheaper and far more cache/page-friendly.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.