C++ 不使用动态内存分配的Pimpl习惯用法
我们希望在项目的某些部分使用pimpl习惯用法。项目的这些部分也恰好是禁止动态内存分配的部分,而这个决定不在我们的控制范围之内 所以我要问的是,有没有一种干净、好的方法可以在不进行动态内存分配的情况下实现pimpl习惯用法 编辑C++ 不使用动态内存分配的Pimpl习惯用法,c++,embedded,dynamic-memory-allocation,pimpl-idiom,C++,Embedded,Dynamic Memory Allocation,Pimpl Idiom,我们希望在项目的某些部分使用pimpl习惯用法。项目的这些部分也恰好是禁止动态内存分配的部分,而这个决定不在我们的控制范围之内 所以我要问的是,有没有一种干净、好的方法可以在不进行动态内存分配的情况下实现pimpl习惯用法 编辑 这里还有一些其他限制:嵌入式平台、标准C++98、没有外部库、没有模板。pimpl基于指针,您可以将它们设置到分配对象的任何位置。这也可以是cpp文件中声明的对象的静态表。pimpl的要点是保持接口稳定并隐藏实现(及其使用的类型)。一种方法是在类中使用char[]数组。
这里还有一些其他限制:嵌入式平台、标准C++98、没有外部库、没有模板。pimpl基于指针,您可以将它们设置到分配对象的任何位置。这也可以是cpp文件中声明的对象的静态表。pimpl的要点是保持接口稳定并隐藏实现(及其使用的类型)。一种方法是在类中使用char[]数组。使其足够大以适合您的Impl,并在构造函数中,在数组中就地实例化您的Impl,放置为new:
new(&array[0])Impl(…)
您还应该确保没有任何对齐问题,可能是让您的char[]数组成为联合的成员。这:
联合{
字符数组[xxx];
int i;
双d;
char*p;
};代码>
例如,将确保<代码>数组[0 ] < /代码>将适用于int、双或指针。
参见和使用固定分配器以及piml习惯用法。 如果可以使用Boost,请考虑。这避免了动态分配的成本,但同时,在您认为必要之前不会构造对象。警告:此处的代码仅显示存储方面,它是一个框架,未考虑动态方面(构造、复制、移动、销毁)
我建议使用C++0x新类的方法,该类正好用于原始存储
// 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;
};
//头
福班
{
公众:
私人:
结构Impl;
Impl&Impl(){return reinterpret_cast(_storage);}
Impl const&Impl()const{return reinterpret_cast(_storage);}
静态常量大小\u t StorageSize=XXX;
静态常量大小\u t StorageAlign=YYY;
std::对齐存储::类型存储;
};
然后,在源代码中执行检查:
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
}
structfoo::Impl{…};
Foo::Foo()
{
//10%公差余量
static_assert(sizeof(Impl)使用pimpl的目的是隐藏对象的实现。这包括真正实现对象的大小。但是,这也使得避免动态分配变得很困难-为了为对象保留足够的堆栈空间,您需要知道对象有多大
典型的解决方案确实是使用动态分配,并将分配足够空间的责任传递给(隐藏的)实现。但是,在您的情况下,这是不可能的,因此我们需要另一种选择
一个这样的选项是使用<代码> AlOLAL()//>代码。这个鲜为人知的函数在堆栈上分配内存;当函数退出其范围时,内存将自动释放。这不是可移植的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();
}
这并不像上面的方法那样纯粹,因为每当实现大小发生变化时,您都必须更改头。但是,它允许您使用普通语法进行初始化
还可以实现一个影子堆栈,即与普通C++堆栈分离的一个二级堆栈,专门用来保存piml对象。这需要非常仔细的管理,但是,如果正确地包装,它应该工作。这种类型处于动态和静态分配之间的灰色区域。
// 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();
}
//每个线程一个实例;TLS留给读者作为练习
类阴影堆栈{
字符堆栈[4096];
ssize_t ptr;
公众:
阴影堆栈(){
ptr=尺寸(堆栈);
}
~ShadowStack(){
断言(ptr==sizeof(stack));
}
空*alloc(尺寸×深){
if(sz%8)//将8替换为平台的最大对齐
sz+=8-(sz%8);
if(ptrss=ss;
if(ss)
pImpl=ss->alloc(sizeof(FooImpl));
其他的
pImpl=新的FooImpl();
}
Foo::~Foo(){
if(ss)
ss->free(pImpl,sizeof(FooImpl));
其他的
删除ss;
}
void callFoo(){
foox(和theStack);
x、 Foo();
}
使用这种方法时,确保不在包装器对象位于堆上的对象上使用阴影堆栈是至关重要的;这将违反对象总是按与创建相反的顺序销毁的假设。我使用的一种技术是非自有pImpl包装器。这是一个非常小的选项,不像trad那样安全传统的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.
}
/*标题*/
结构MyClassPimpl;
结构MyClass{
MyClass(MyClassPimpl&stack_object);//使用stack对象初始化包装器。
私人:
MyClassPimpl*mImpl;//您也可以使用ref。
};
/*在实现代码的某个地方*/
void func(常量std::函数和回调){
MyClassPimpl;//初始化堆栈上的pimpl。
MyClass obj(p);//创建包装器。
回调(obj);//使用MyClass obj调用用户代码。
}
与大多数包装器一样,这里的危险在于用户将包装器存储在比堆栈分配更有效的范围内。使用时请自行承担风险<
/* 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.
}