使用 pimpl 的目的是隐藏对象的实现。这包括真正的实现对象的size。然而,这也使得避免动态分配变得很尴尬——为了为对象保留足够的堆栈空间,您需要知道对象有多大。
典型的解决方案确实是使用动态分配,并将分配足够空间的责任交给(隐藏的)实现。但是,这在您的情况下是不可能的,因此我们需要另一种选择。
其中一个选项是使用alloca()。这个鲜为人知的函数在栈上分配内存;当函数退出其作用域时,内存将被自动释放。 这不是可移植的 C++,但是许多 C++ 实现都支持它(或者这个想法的变体)。
请注意,您必须使用宏分配 pimpl 对象;必须调用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 的对象。这需要非常仔细的管理,但是,如果包装得当,它应该可以工作。这种类型处于动态分配和静态分配之间的灰色地带。
// 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();
}
使用这种方法,确保您不会将影子堆栈用于包装对象位于堆上的对象,这一点至关重要;这将违反对象总是以相反的创建顺序销毁的假设。