我们希望在项目的某些部分使用 pimpl 惯用语。项目的这些部分也恰好是禁止动态内存分配的部分,并且这个决定不在我们的控制范围内。
所以我要问的是,有没有一种干净又好的方法来实现 pimpl 惯用语而不需要动态内存分配?
编辑
以下是一些其他限制:嵌入式平台、标准 C++98、无外部库、无模板。
警告:这里的代码仅展示了存储方面,它是一个骨架,没有考虑动态方面(构造、复制、移动、销毁)。
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
}
这样,虽然您必须立即更改对齐方式(如有必要),但只有当对象变化太大时,尺寸才会改变。
显然,由于检查是在编译时进行的,所以你不能错过它:)
aligned_storage
和 alignof
的等效项,并且有 static_assert
的宏实现。
pimpl 基于指针,您可以将它们设置到分配对象的任何位置。这也可以是 cpp 文件中声明的对象的静态表。 pimpl 的要点是保持接口稳定并隐藏实现(及其使用的类型)。
请参阅快速 Pimpl 惯用法和Pimpls 的乐趣了解如何将固定分配器与 pimpl 惯用法一起使用。
boost::optional<>
。这避免了动态分配的成本,但同时,除非您认为有必要,否则不会构造您的对象。
一种方法是在你的类中使用 char[] 数组。使其足够大以适合您的 Impl,并在构造函数中,在数组中实例化您的 Impl,并使用新的位置:
new (&array[0]) Impl(...)
。
您还应该确保您没有任何对齐问题,可能是通过让您的 char[] 数组成为联合的成员。这个:
union {
char array[xxx];
int i;
double d;
char *p;
};
例如,将确保
array[0]
的对齐方式适合 int、double 或指针。
使用 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 类似。
如果您可以在标头中硬编码对象的大小,还可以选择使用字符数组:
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.
}
与大多数包装器一样,这里的危险是用户将包装器存储在一个比堆栈分配寿命更长的范围中。使用需要您自担风险。