Is pimpl as bad as virtual ? This is really a bad design ?
Here's two examples to compare.
First, using vtables, implemented manually instead of using virtual so we can see what's going on:
class Foo;
struct Foo_VTable
{
typedef void (Foo::*FnDoStuff)(int a, int b);
FnDoStuff fnDoStuff;
};
class Foo
{
public:
void DoStuff( int a, int b )
{
((this)->*(vtable->fnDoStuff))(a, b);
}
protected:
const Foo_VTable* vtable;
};
class Foo_Derived : public Foo
{
public:
Foo_Derived() : value(0)
{
const static Foo_VTable s_vtable =
{
(Foo_VTable::FnDoStuff)&Foo_Derived::DoStuff
};
vtable = &s_vtable;
}
private:
void DoStuff( int a, int b )
{
value = value * a + b;
}
int value;
};
void test_vtable()
{
Foo_Derived d;
Foo* f = &d;
f->DoStuff(1, 2);
//reads v = f->vtable
//reads fn = v->fnDoStuff
//calls fn
//reads/writes f->value
}
Second, using PIMPL:
class Bar_Impl;
class Bar
{
public:
Bar();
void DoStuff( int a, int b );
private:
Bar_Impl* pimpl;
};
class Bar_Impl
{
public:
Bar_Impl() : value(0) {}
void DoStuff( int a, int b )
{
value = value * a + b;
}
private:
int value;
};
Bar::Bar() { pimpl = new Bar_Impl; }
void Bar::DoStuff( int a, int b ) { pimpl->DoStuff(a, b); }
void test_pimpl()
{
Bar b;
b.DoStuff(1, 2);
//reads p = b.pmpl
//reads/writes p->value
}
The important operations -- the memory access patterns are shown in comments in the test functions:
vtable:
//reads v = f->vtable
//reads fn = v->fnDoStuff
//calls fn
//reads/writes f->value
pimpl:
//reads p = b.pmpl
//reads/writes p->value
Both of them suffer from a "
double indirection".
The vtable method first has to access the object to get the address of the vtable. Only when that's complete can it access the vtable to find out which function to call. This is likely to cause a cache miss and stall the CPU.
Inside the function it accesses the 'value' member, but that won't cause a cache miss because it lives right next to the address of the vtable, which we fetched initially.
The pimpl method first has to access the wrapper object to get the address of the implementation object. Only when that's complete can it access the 'value' member. Again, this double indirection increases the chance of causing a cache-miss and stalling the CPU.