The point of using pimpl is to hide the implementation of your object. This includes the size of the true implementation object. However, it also makes it inconvenient to avoid dynamic allocation - in order to reserve enough stack space for an object, you need to know how large the object is.
A typical solution is to use dynamic allocation and transfer responsibility for allocating sufficient space for (hidden) implementation. However, this is not possible in your case, so we will need another option.
One such parameter uses alloca() . This little-known function allocates memory on the stack; the memory will be automatically freed when the function goes out of scope. This is not portable C ++, however, many C ++ implementations support it (or a variation of this idea).
Note that you must select pimpl'd objects with a macro; alloca() must be called to get the necessary memory directly from its own function. Example:
// Foo.h class Foo { void *pImpl; public: void bar(); static const size_t implsz_; Foo(void *); ~Foo(); }; #define DECLARE_FOO(name) \ Foo name(alloca(Foo::implsz_)); // Foo.cpp class FooImpl { void bar() { std::cout << "Bar!\n"; } }; Foo::Foo(void *pImpl) { this->pImpl = pImpl; new(this->pImpl) FooImpl; } Foo::~Foo() { ((FooImpl*)pImpl)->~FooImpl(); } void Foo::Bar() { ((FooImpl*)pImpl)->Bar(); } // Baz.cpp void callFoo() { DECLARE_FOO(x); x.bar(); }
This, as you can see, makes the syntax rather inconvenient, but it does the analog of pimpl.
If you can hardcode the size of the object in the header, you can also use the char array:
class Foo { private: enum { IMPL_SIZE = 123; }; union { char implbuf[IMPL_SIZE]; double aligndummy;
This is less clean than the approach described above, since you must change the headers whenever the implementation size changes. However, it allows you to use the usual syntax for initialization.
You can also implement a shadow stack, that is, an extra stack separate from the regular C ++ stack, in particular for storing pImpl'd objects. This requires very careful management, but, properly wrapped, it should work. This type is in the gray zone between dynamic and static distribution.
// One instance per thread; TLS is left as an exercise for the reader class ShadowStack { char stack[4096]; ssize_t ptr; public: ShadowStack() { ptr = sizeof(stack); } ~ShadowStack() { assert(ptr == sizeof(stack)); } void *alloc(size_t sz) { if (sz % 8) // replace 8 with max alignment for your platform sz += 8 - (sz % 8); if (ptr < sz) return NULL; ptr -= sz; return &stack[ptr]; } void free(void *p, size_t sz) { assert(p == stack[ptr]); ptr += sz; assert(ptr < sizeof(stack)); } }; ShadowStack theStack; Foo::Foo(ShadowStack *ss = NULL) { this->ss = ss; if (ss) pImpl = ss->alloc(sizeof(FooImpl)); else pImpl = new FooImpl(); } Foo::~Foo() { if (ss) ss->free(pImpl, sizeof(FooImpl)); else delete ss; } void callFoo() { Foo x(&theStack); x.Foo(); }
With this approach, it is extremely important to make sure that you DO NOT use the shadow stack for objects where the wrapper object is on the heap; this violates the assumption that objects are always destroyed in the reverse order of creation.