我们想在项目的某些部分使用 pimpl idiom。项目的这些部分也恰好是禁止动态内存分配的部分,这个决定不在我们的控制范围内。
所以我要问的是,有没有一种干净而好的方法来实现 pimpl idiom 而无需动态内存分配?
编辑
这里有一些其他限制:嵌入式平台,标准 C++98,没有外部库,没有模板。
我们想在项目的某些部分使用 pimpl idiom。项目的这些部分也恰好是禁止动态内存分配的部分,这个决定不在我们的控制范围内。
所以我要问的是,有没有一种干净而好的方法来实现 pimpl idiom 而无需动态内存分配?
编辑
这里有一些其他限制:嵌入式平台,标准 C++98,没有外部库,没有模板。
警告:这里的代码只展示了存储方面,它是一个骨架,没有考虑动态方面(构造、复制、移动、销毁)。
我会建议一种使用 C++0x 新类的方法aligned_storage
,这正是用于拥有原始存储的。
// header
class Foo
{
public:
private:
struct Impl;
Impl& impl() { return reinterpret_cast<Impl&>(_storage); }
Impl const& impl() const { return reinterpret_cast<Impl const&>(_storage); }
static const size_t StorageSize = XXX;
static const size_t StorageAlign = YYY;
std::aligned_storage<StorageSize, StorageAlign>::type _storage;
};
然后在源代码中实施检查:
struct Foo::Impl { ... };
Foo::Foo()
{
// 10% tolerance margin
static_assert(sizeof(Impl) <= StorageSize && StorageSize <= sizeof(Impl) * 1.1,
"Foo::StorageSize need be changed");
static_assert(StorageAlign == alignof(Impl),
"Foo::StorageAlign need be changed");
/// anything
}
这样,虽然您必须立即更改对齐方式(如有必要),但仅当对象更改太多时,大小才会更改。
显然,由于检查是在编译时进行的,所以你不能错过它:)
如果您无权访问 C++0x 功能,则 TR1 命名空间中有aligned_storage
和的等价物,alignof
并且有static_assert
.
pimpl 基于指针,您可以将它们设置到分配对象的任何位置。这也可以是在 cpp 文件中声明的对象的静态表。pimpl 的主要目的是保持接口稳定并隐藏实现(及其使用的类型)。
请参阅 The Fast Pimpl Idiom和The Joy of Pimpls,了解如何将固定分配器与 pimpl idiom 一起使用。
如果可以使用 boost,请考虑boost::optional<>
. 这避免了动态分配的成本,但同时,除非您认为有必要,否则不会构造您的对象。
一种方法是在你的类中有一个 char[] 数组。使其足够大以适合您的 Impl,并在您的构造函数中,将您的 Impl 实例化在数组中的适当位置,并放置 new: new (&array[0]) Impl(...)
。
您还应该确保您没有任何对齐问题,可能通过让您的 char[] 数组成为联合的成员。这个:
union {
char array[xxx];
int i;
double d;
char *p;
};
例如,将确保 的对齐方式array[0]
适用于 int、double 或指针。
使用 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 的对象。这需要非常仔细的管理,但是,如果包装得当,它应该可以工作。这种类型处于动态和静态分配之间的灰色地带。
// 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();
}
使用这种方法,确保您不会对包装对象位于堆上的对象使用影子堆栈是至关重要的;这将违反对象总是以相反的创建顺序销毁的假设。
我使用的一种技术是非拥有 pImpl 包装器。这是一个非常小众的选择,不如传统的 pimpl 安全,但如果性能是一个问题,它会有所帮助。它可能需要一些重新架构才能像 api 一样具有更多功能。
您可以创建一个非拥有的 pimpl 类,只要您可以(在某种程度上)保证堆栈 pimpl 对象的寿命将超过包装器。
例如。
/* header */
struct MyClassPimpl;
struct MyClass {
MyClass(MyClassPimpl& stack_object); // Initialize wrapper with stack object.
private:
MyClassPimpl* mImpl; // You could use a ref too.
};
/* in your implementation code somewhere */
void func(const std::function<void()>& callback) {
MyClassPimpl p; // Initialize pimpl on stack.
MyClass obj(p); // Create wrapper.
callback(obj); // Call user code with MyClass obj.
}
像大多数包装器一样,这里的危险是用户将包装器存储在一个范围内,该范围将超过堆栈分配。使用风险自负。