简体   繁体   English

不使用动态内存分配的 Pimpl 习惯用法

[英]Pimpl idiom without using dynamic memory allocation

we want to use pimpl idiom for certain parts of our project.我们想在项目的某些部分使用 pimpl idiom。 These parts of the project also happen to be parts where dynamic memory allocation is forbidden and this decision is not in our control.项目的这些部分也恰好是禁止动态内存分配的部分,这个决定不在我们的控制范围内。

So what i am asking is, is there a clean and nice way of implementing pimpl idiom without dynamic memory allocation?所以我要问的是,有没有一种干净而漂亮的方法可以在没有动态内存分配的情况下实现 pimpl idiom?

Edit编辑
Here are some other limitations: Embedded platform, Standard C++98, no external libraries, no templates.以下是一些其他限制:嵌入式平台、标准 C++98、无外部库、无模板。

Warning: the code here only showcases the storage aspect, it is a skeleton, no dynamic aspect (construction, copy, move, destruction) has been taken into account.警告:这里的代码只展示了存储方面,它是一个框架,没有考虑动态方面(构造、复制、移动、销毁)。

I would suggest an approach using the C++0x new class aligned_storage , which is precisely meant for having raw storage.我会建议使用 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;
};

In the source, you then implement a check:然后在源代码中实施检查:

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
}

This way, while you'll have to change the alignment immediately (if necessary) the size will only change if the object changes too much.这样,虽然您必须立即更改对齐方式(如有必要),但只有当对象更改太多时,大小才会更改。

And obviously, since the check is at compilation time, you just cannot miss it:)显然,由于检查是在编译时进行的,所以你不能错过它:)

If you do not have access to C++0x features, there are equivalents in the TR1 namespace for aligned_storage and alignof and there are macros implementations of static_assert .如果您无权访问 C++0x 功能,则在 TR1 命名空间中有aligned_storagealignof的等效项,并且有static_assert的宏实现。

pimpl bases on pointers and you can set them to any place where your objects are allocated. pimpl 基于指针,您可以将它们设置到分配对象的任何位置。 This can also be a static table of objects declared in the cpp file.这也可以是在 cpp 文件中声明的静态对象表。 The main point of pimpl is to keep the interfaces stable and hide the implementation (and its used types). pimpl 的要点是保持接口稳定并隐藏实现(及其使用的类型)。

See The Fast Pimpl Idiom and The Joy of Pimpls about using a fixed allocator along with the pimpl idiom.请参阅The Fast Pimpl IdiomThe Joy of Pimpls关于使用固定分配器和 pimpl 惯用语。

If you can use boost, consider boost::optional<> .如果可以使用 boost,请考虑boost::optional<> This avoids the cost of dynamic allocation, but at the same time, your object will not be constructed until you deem necessary.这避免了动态分配的成本,但同时,除非您认为有必要,否则不会构造您的对象。

One way would be to have a char[] array in your class.一种方法是在你的类中有一个 char[] 数组。 Make it large enough for your Impl to fit, and in your constructor, instantiate your Impl in place in your array, with a placement new: new (&array[0]) Impl(...) .使其足够大以适合您的 Impl,并在您的构造函数中实例化您的 Impl 在数组中的位置,并放置 new: new (&array[0]) Impl(...)

You should also ensure you don't have any alignment problems, probably by having your char[] array a member of an union.您还应该确保您没有任何对齐问题,可能是通过让您的 char[] 数组成为联合的成员。 This:这个:

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

for instance, will make sure the alignment of array[0] will be suitable for an int, double or a pointer.例如,将确保array[0]的对齐适合 int、double 或指针。

The point of using pimpl is to hide the implementation of your object.使用 pimpl 的目的是隐藏对象的实现。 This includes the size of the true implementation object.这包括真实实现对象的大小 However this also makes it awkward to avoid dynamic allocation - in order to reserve sufficient stack space for the object, you need to know how big the object is.然而这也使得避免动态分配变得尴尬——为了为对象预留足够的栈空间,你需要知道对象有多大。

The typical solution is indeed to use dynamic allocation, and pass the responsibility for allocating sufficient space to the (hidden) implementation.典型的解决方案确实是使用动态分配,并将分配足够空间的责任传递给(隐藏)实现。 However, this isn't possible in your case, so we'll need another option.但是,这在您的情况下是不可能的,因此我们需要另一种选择。

One such option is using alloca() .一种选择是使用alloca() This little-known function allocates memory on the stack;这个鲜为人知的函数在栈上分配内存; the memory will be automatically freed when the function exits its scope.当函数退出其范围时,内存将自动释放。 This is not portable C++ , however many C++ implementations support it (or a variation on this idea).这不是可移植的 C++ ,但是许多 C++ 实现支持它(或这个想法的变体)。

Note that you must allocate your pimpl'd objects using a macro;请注意,您必须使用宏分配您的 pimpl 对象; alloca() must be invoked to obtain the necessary memory directly from the owning function.必须调用alloca()以直接从所属函数获取必要的内存。 Example:例子:

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

This, as you can see, makes the syntax rather awkward, but it does accomplish a pimpl analogue.如您所见,这使得语法相当笨拙,但它确实完成了一个 pimpl 类比。

If you can hardcode the size of the object in the header, there's also the option of using a char array:如果您可以在标头中硬编码对象的大小,则还可以选择使用 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;
// ...
}

This is less pure than the above approach, as you must change the headers whenever the implementation size changes.这不如上述方法纯粹,因为只要实现大小发生变化,您就必须更改标头。 However, it allows you to use normal syntax for initialization.但是,它允许您使用正常语法进行初始化。

You could also implement a shadow stack - that is, a secondary stack separate from the normal C++ stack, specifically to hold pImpl'd objects.您还可以实现影子堆栈——即与普通 C++ 堆栈分开的辅助堆栈,专门用于保存 pImpl 对象。 This requires very careful management, but, properly wrapped, it should work.这需要非常仔细的管理,但是,如果包装得当,它应该可以工作。 This sort of is in the grey zone between dynamic and static allocation.这种类型处于动态和静态分配之间的灰色地带。

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

With this approach it is critical to ensure that you do NOT use the shadow stack for objects where the wrapper object is on the heap;使用这种方法,确保不对包装对象在堆上的对象使用影子堆栈是至关重要的; this would violate the assumption that objects are always destroyed in reverse order of creation.这将违反对象总是以相反的创建顺序销毁的假设。

One technique I've used is a non-owning pImpl wrapper.我使用的一种技术是非拥有 pImpl 包装器。 This is a very niche option and isn't as safe as traditional pimpl, but it can help if performance is a concern.这是一个非常小众的选择,不像传统的粉刺那样安全,但如果性能是一个问题,它会有所帮助。 It may require some re-architecture to more functional like apis.它可能需要一些重新架构才能像 api 一样具有更多功能。

You can create a non-owning pimpl class, as long as you can (somewhat) guarantee the stack pimpl object will outlive the wrapper.您可以创建一个非拥有的 pimpl 类,只要您可以(某种程度上)保证堆栈 pimpl 对象的寿命比包装器长。

For ex.对于前。

/* 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.
}

The danger here, like most wrappers, is the user stores the wrapper in a scope that will outlive the stack allocation.与大多数包装器一样,这里的危险是用户将包装器存储在一个范围内,该范围将超过堆栈分配。 Use at your own risk.使用风险自负。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM