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

发布于 2024-10-16 03:29:03 字数 177 浏览 3 评论 0 原文

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

所以我要问的是,是否有一种干净、漂亮的方法来实现 pimpl 惯用语而不需要动态内存分配?

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

we want to use pimpl idiom for certain parts of our project. 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?

Edit
Here are some other limitations: Embedded platform, Standard C++98, no external libraries, no templates.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

夜司空 2024-10-23 03:29:03

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

我建议使用 C+ 的方法+0x new class 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_storagealignof 并且有 static_assert

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.

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

以歌曲疗慰 2024-10-23 03:29:03

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

pimpl bases on pointers and you can set them to any place where your objects are allocated. This can also be a static table of objects declared in the cpp file. The main point of pimpl is to keep the interfaces stable and hide the implementation (and its used types).

季末如歌 2024-10-23 03:29:03

请参阅快速 Pimpl 惯用法Pimpls 的乐趣 关于使用固定分配器和 pimpl 习惯用法。

See The Fast Pimpl Idiom and The Joy of Pimpls about using a fixed allocator along with the pimpl idiom.

眉黛浅 2024-10-23 03:29:03

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

If you can use boost, consider boost::optional<>. This avoids the cost of dynamic allocation, but at the same time, your object will not be constructed until you deem necessary.

忆梦 2024-10-23 03:29:03

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

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

联合{
字符数组[xxx];
整数我;
双 d;
字符*p;
例如

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

One way would be to have a char[] array in your class. 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(...).

You should also ensure you don't have any alignment problems, probably by having your char[] array a member of an union. 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.

Oo萌小芽oO 2024-10-23 03:29:03

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

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

其中一种选择是使用alloca()。这个鲜为人知的函数在堆栈上分配内存;当函数退出其作用域时,内存将自动释放。 这不是可移植的 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();
}

正如您所看到的,这使得语法相当尴尬,但它确实实现了 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();
}

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

The point of using pimpl is to hide the implementation of your object. 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(). 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).

Note that you must allocate your pimpl'd objects using a macro; alloca() must be invoked to obtain the necessary memory directly from the owning function. 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.

If you can hardcode the size of the object in the header, there's also the option of using a char array:

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. 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.

土豪 2024-10-23 03:29:03

我使用的一种技术是非拥有 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.
}

与大多数包装器一样,这里的危险是用户将包装器存储在一个比堆栈分配寿命更长的范围中。使用风险自负。

One technique I've used is a non-owning pImpl wrapper. 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.

You can create a non-owning pimpl class, as long as you can (somewhat) guarantee the stack pimpl object will outlive the wrapper.

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文