30

プロジェクトの特定の部分に pimpl イディオムを使用したいと考えています。プロジェクトのこれらの部分は、たまたま動的メモリ割り当てが禁止されている部分でもあり、この決定は私たちの管理下にありません。

だから私が求めているのは、動的メモリ割り当てなしでpimplイディオムを実装するクリーンで良い方法はありますか?

編集
その他の制限事項: 組み込みプラットフォーム、標準 C++98、外部ライブラリなし、テンプレートなし。

4

7 に答える 7

30

警告: ここのコードはストレージの側面のみを示しています。これはスケルトンであり、動的な側面 (構築、コピー、移動、破棄) は考慮されていません。

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_storageとの同等物alignofがあり、 のマクロ実装がありますstatic_assert

于 2011-02-07T14:20:27.900 に答える
9

pimpl はポインタに基づいており、オブジェクトが割り当てられている任意の場所に設定できます。これは、cpp ファイルで宣言されたオブジェクトの静的テーブルにすることもできます。pimpl の主なポイントは、インターフェイスを安定させ、実装 (およびその使用される型) を隠すことです。

于 2011-02-07T13:41:56.977 に答える
4

pimpl イディオムと一緒に固定アロケータを使用する方法については、 The Fast Pimpl IdiomおよびThe Joy of Pimplsを参照 してください。

于 2011-02-07T13:47:08.580 に答える
4

ブーストを使用できる場合は、 を検討してboost::optional<>ください。これにより、動的割り当てのコストが回避されますが、同時に、必要と判断されるまでオブジェクトは構築されません。

于 2011-02-07T14:11:27.520 に答える
3

1 つの方法は、クラスに char[] 配列を含めることです。Impl が収まるのに十分な大きさにし、コンストラクターで、配置 new: を使用して、配列内の適切な場所に Impl をインスタンス化します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 を使用するポイントは、オブジェクトの実装を隠すことです。これには、真の実装オブジェクトのサイズが含まれます。ただし、これにより、動的割り当てを回避することが難しくなります。オブジェクトに十分なスタック領域を確保するには、オブジェクトの大きさを知る必要があります。

典型的な解決策は、確かに動的割り当てを使用し、十分なスペースを割り当てる責任を (隠された) 実装に渡すことです。ただし、これはあなたの場合には不可能であるため、別のオプションが必要になります。

そのようなオプションの 1 つが使用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();
}

このアプローチでは、ラッパー オブジェクトがヒープ上にあるオブジェクトにシャドウ スタックを使用しないようにすることが重要です。これは、オブジェクトが常に作成と逆の順序で破棄されるという仮定に違反します。

于 2011-02-07T14:36:39.613 に答える
0

私が使用したテクニックの 1 つは、非所有 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 に答える