C++ 不使用动态内存分配的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[]数组。

我们希望在项目的某些部分使用pimpl习惯用法。项目的这些部分也恰好是禁止动态内存分配的部分,而这个决定不在我们的控制范围之内

所以我要问的是,有没有一种干净、好的方法可以在不进行动态内存分配的情况下实现pimpl习惯用法

编辑

这里还有一些其他限制:嵌入式平台、标准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.
}