Смысл использования pimpl в том, чтобы скрыть реализацию вашего объекта.Это включает размер истинного объекта реализации.Однако это также делает неудобным избегание динамического выделения - для того, чтобы зарезервировать достаточно стекового пространства для объекта, вам необходимо знать, насколько велик объект.
Типичным решением действительно является использование динамического выделения и передачаответственность за выделение достаточного пространства для (скрытой) реализации.Однако в вашем случае это невозможно, поэтому нам понадобится другая опция.
Одна такая опция использует alloca()
.Эта малоизвестная функция выделяет память в стеке;память будет автоматически освобождена, когда функция выйдет из области видимости. Это не переносимый C ++ , однако многие реализации C ++ поддерживают его (или вариант этой идеи).
Обратите внимание, что вы должны размещать объекты pimpl'd с помощью макроса;alloca()
должен быть вызван для получения необходимой памяти непосредственно из функции-владельца.Пример:
// 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();
}
Это, как вы можете видеть, делает синтаксис довольно неловким, но он выполняет аналог pimpl.
Если вы можете жестко задавать размер объекта в заголовкеесть также возможность использования массива char:
class Foo {
private:
enum { IMPL_SIZE = 123; };
union {
char implbuf[IMPL_SIZE];
double aligndummy; // make this the type with strictest alignment on your platform
} impl;
// ...
}
Это менее чисто, чем описанный выше подход, так как вы должны менять заголовки всякий раз, когда изменяется размер реализации.Однако он позволяет вам использовать обычный синтаксис для инициализации.
Вы также можете реализовать теневой стек, то есть вторичный стек, отдельный от обычного стека C ++, специально для хранения объектов pImpl'd.Это требует очень осторожного управления, но при правильной упаковке оно должно работать.Этот вид находится в серой зоне между динамическим и статическим размещением.
// 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();
}
При таком подходе важно убедиться, что вы НЕ используете стека теней для объектов, где объект-оболочка находится в куче;это нарушило бы предположение, что объекты всегда уничтожаются в обратном порядке создания.