Pimpl成语不使用动态内存分配

时间:2011-02-07 13:38:24

标签: c++ embedded dynamic-memory-allocation pimpl-idiom

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

所以我要问的是,在没有动态内存分配的情况下,是否有一种干净又好的方法来实现pimpl习语?

编辑
以下是一些其他限制:嵌入式平台,标准C ++ 98,没有外部库,没有模板。

7 个答案:

答案 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_storagealignof的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 IdiomThe 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.
}

与大多数包装程序一样,这里的危险是用户将包装程序存储在超出堆栈分配寿命的范围内。使用风险自负。