30

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

所以我要问的是,有没有一种干净而好的方法来实现 pimpl idiom 而无需动态内存分配?

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

4

7 回答 7

30

警告:这里的代码只展示了存储方面,它是一个骨架,没有考虑动态方面(构造、复制、移动、销毁)。

我会建议一种使用 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 功能,则 TR1 命名空间中有aligned_storage和的等价物,alignof并且有static_assert.

于 2011-02-07T14:20:27.900 回答
9

pimpl 基于指针,您可以将它们设置到分配对象的任何位置。这也可以是在 cpp 文件中声明的对象的静态表。pimpl 的主要目的是保持接口稳定并隐藏实现(及其使用的类型)。

于 2011-02-07T13:41:56.977 回答
4

请参阅 The Fast Pimpl IdiomThe Joy of Pimpls,了解如何将固定分配器与 pimpl idiom 一起使用。

于 2011-02-07T13:47:08.580 回答
4

如果可以使用 boost,请考虑boost::optional<>. 这避免了动态分配的成本,但同时,除非您认为有必要,否则不会构造您的对象。

于 2011-02-07T14:11:27.520 回答
3

一种方法是在你的类中有一个 char[] 数组。使其足够大以适合您的 Impl,并在您的构造函数中,将您的 Impl 实例化在数组中的适当位置,并放置 new: new (&array[0]) Impl(...)

您还应该确保您没有任何对齐问题,可能通过让您的 char[] 数组成为联合的成员。这个:

union { char array[xxx]; int i; double d; char *p; };

例如,将确保 的对齐方式array[0]适用于 int、double 或指针。

于 2011-02-07T13:42:51.297 回答
1

使用 pimpl 的目的是隐藏对象的实现。这包括真正的实现对象的大小。然而,这也使得避免动态分配变得很尴尬——为了为对象保留足够的堆栈空间,您需要知道对象有多大。

典型的解决方案确实是使用动态分配,并将分配足够空间的责任交给(隐藏的)实现。但是,这在您的情况下是不可能的,因此我们需要另一种选择。

一种这样的选择是使用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 的对象。这需要非常仔细的管理,但是,如果包装得当,它应该可以工作。这种类型处于动态和静态分配之间的灰色地带。

// 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();
}

使用这种方法,确保您不会对包装对象位于堆上的对象使用影子堆栈是至关重要的;这将违反对象总是以相反的创建顺序销毁的假设。

于 2011-02-07T14:36:39.613 回答
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.
}

像大多数包装器一样,这里的危险是用户将包装器存储在一个范围内,该范围将超过堆栈分配。使用风险自负。

于 2020-07-31T19:55:15.860 回答