我们想对项目的某些部分使用pimpl习语。项目的这些部分也恰好是禁止动态内存分配的部分,这个决定不在我们的控制范围内。
所以我要问的是,在没有动态内存分配的情况下,是否有一种干净又好的方法来实现pimpl习语?
编辑
以下是一些其他限制:嵌入式平台,标准C ++ 98,没有外部库,没有模板。
答案 0 :(得分:25)
警告:此处的代码仅显示存储方面,它是一个骨架,没有考虑动态方面(构造,复制,移动,破坏)。
我建议使用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功能,则aligned_storage
和alignof
的TR1名称空间中存在等效项,并且存在static_assert
的宏实现。
答案 1 :(得分:8)
pimpl基于指针,您可以将它们设置到分配对象的任何位置。这也可以是cpp文件中声明的对象的静态表。 pimpl的要点是保持接口稳定并隐藏实现(及其使用的类型)。
答案 2 :(得分:4)
如果您可以使用提升,请考虑boost::optional<>
。这样可以避免动态分配的成本,但与此同时,在您认为必要之前,不会构建对象。
答案 3 :(得分:3)
一种方法是在类中使用char []数组。使它足够大,以使你的Impl适合,并在你的构造函数中,在你的数组中实例化你的Impl,并使用一个新的位置:new (&array[0]) Impl(...)
。
您还应该确保没有任何对齐问题,可能是将char []数组作为union的成员。这样:
union {
char array[xxx];
int i;
double d;
char *p;
};
将确保array[0]
的对齐适合int,double或指针。
答案 4 :(得分:3)
有关使用固定分配器和pimpl习惯用语的信息,请参阅The Fast Pimpl Idiom和The Joy of Pimpls。
答案 5 :(得分:1)
使用pimpl的目的是隐藏对象的实现。这包括真实实现对象的 size 。然而,这也使得避免动态分配变得尴尬 - 为了为对象保留足够的堆栈空间,您需要知道对象有多大。
典型的解决方案确实是使用动态分配,并将责任分配给(隐藏)实现。但是,在您的情况下这是不可能的,因此我们需要另一个选项。
一个这样的选择是使用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();
}
使用这种方法,确保不对包装器对象在堆上的对象使用阴影堆栈至关重要;这违反了对象总是以相反的创建顺序销毁的假设。
答案 6 :(得分:0)
我使用的一种技术是非所有者的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.
}
与大多数包装程序一样,这里的危险是用户将包装程序存储在超出堆栈分配寿命的范围内。使用风险自负。