9

次の点を考慮してください。

PImpl.hpp

class Impl;

class PImpl
{
    Impl* pimpl;
    PImpl() : pimpl(new Impl) { }
    ~PImpl() { delete pimpl; }
    void DoSomething();
};

PImpl.cpp

#include "PImpl.hpp"
#include "Impl.hpp"

void PImpl::DoSomething() { pimpl->DoSomething(); }

実装.hpp

class Impl
{
    int data;
public:
    void DoSomething() {}
}

client.cpp

#include "Pimpl.hpp"

int main()
{
    PImpl unitUnderTest;
    unitUnderTest.DoSomething();
}

このパターンの背後にある考え方は、Implのインターフェースは変更できますが、クライアントを再コンパイルする必要はないというものです。しかし、これが実際にどのように当てはまるのか、私にはわかりません。このクラスにメソッドを追加したいとしましょう。クライアントはまだ再コンパイルする必要があります。

基本的に、クラスのヘッダー ファイルを変更する必要があると思われるこのような変更は、クラスのインターフェイスが変更されるものだけです。そして、それが発生した場合、pimpl の有無にかかわらず、クライアントは再コンパイルする必要があります。

クライアント コードを再コンパイルしないという点で、ここでどのような種類の編集を行うとメリットがありますか?

4

7 に答える 7

10

主な利点は、インターフェイスのクライアントが、クラスのすべての内部依存関係のヘッダーを含めることを強制されないことです。したがって、これらのヘッダーへの変更は、ほとんどのプロジェクトの再コンパイルにカスケードしません。さらに、実装の非表示に関する一般的な理想主義。

また、必ずしも impl クラスを独自のヘッダーに入れる必要はありません。単一の cpp 内の構造体にして、外部クラスがそのデータ メンバーを直接参照するようにするだけです。

編集:

SomeClass.h

struct SomeClassImpl;

class SomeClass {
    SomeClassImpl * pImpl;
public:
    SomeClass();
    ~SomeClass();
    int DoSomething();
};

SomeClass.cpp

#include "SomeClass.h"
#include "OtherClass.h"
#include <vector>

struct SomeClassImpl {
    int foo;
    std::vector<OtherClass> otherClassVec;   //users of SomeClass don't need to know anything about OtherClass, or include its header.
};

SomeClass::SomeClass() { pImpl = new SomeClassImpl; }
SomeClass::~SomeClass() { delete pImpl; }

int SomeClass::DoSomething() {
    pImpl->otherClassVec.push_back(0);
    return pImpl->otherClassVec.size();
}
于 2010-08-30T03:38:47.570 に答える
7

多くの答えがありました...しかし、これまでのところ正しい実装はありません。人々はそれらを使用する可能性が高いため、例が正しくないことは少し悲しいです...

"Pimpl" イディオムは "Pointer to Implementation" の略で、"Compilation Firewall" とも呼ばれます。それでは、飛び込みましょう。

1. インクルードはいつ必要ですか?

クラスを使用する場合、次の場合にのみ完全な定義が必要です。

  • そのサイズが必要です(クラスの属性)
  • そのメソッドの1つにアクセスする必要があります

それを参照するか、それへのポインターを持っているだけの場合、参照またはポインターのサイズは参照/指している型に依存しないため、識別子を宣言するだけで済みます (前方宣言)。

例:

#include "a.h"
#include "b.h"
#include "c.h"
#include "d.h"
#include "e.h"
#include "f.h"

struct Foo
{
  Foo();

  A a;
  B* b;
  C& c;
  static D d;
  friend class E;
  void bar(F f);
};

上記の例では、どのインクルードが「便利な」インクルードであり、正確性に影響を与えずに削除できますか? 最も驚くべきことは、「ああ」以外のすべてです。

2. Pimpl の実装

したがって、Pimpl の考え方は、ヘッダーを含める必要がないように、実装クラスへのポインターを使用することです。

  • したがって、依存関係からクライアントを分離します
  • したがって、コンパイルの波及効果を防ぎます

追加の利点: ライブラリの ABI が保持されます。

使いやすさのために、Pimpl イディオムを「スマート ポインター」管理スタイルで使用できます。

// From Ben Voigt's remark
// information at:
// http://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Checked_delete
template<class T> 
inline void checked_delete(T * x)
{
    typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
    (void) sizeof(type_must_be_complete);
    delete x;
}


template <typename T>
class pimpl
{
public:
  pimpl(): m(new T()) {}
  pimpl(T* t): m(t) { assert(t && "Null Pointer Unauthorized"); }

  pimpl(pimpl const& rhs): m(new T(*rhs.m)) {}

  pimpl& operator=(pimpl const& rhs)
  {
    std::auto_ptr<T> tmp(new T(*rhs.m)); // copy may throw: Strong Guarantee
    checked_delete(m);
    m = tmp.release();
    return *this;
  }

  ~pimpl() { checked_delete(m); }

  void swap(pimpl& rhs) { std::swap(m, rhs.m); }

  T* operator->() { return m; }
  T const* operator->() const { return m; }

  T& operator*() { return *m; }
  T const& operator*() const { return *m; }

  T* get() { return m; }
  T const* get() const { return m; }

private:
  T* m;
};

template <typename T> class pimpl<T*> {};
template <typename T> class pimpl<T&> {};

template <typename T>
void swap(pimpl<T>& lhs, pimpl<T>& rhs) { lhs.swap(rhs); }

他の人が持っていないものは何ですか?

  • これは単純に 3 つのルールに従います: コピー コンストラクター、コピー代入演算子、およびデストラクターを定義します。
  • 強力な保証を実装してそうします: 割り当て中にコピーがスローされた場合、オブジェクトは変更されません。のデストラクタはTスローすべきではないことに注意してください...しかし、それは非常に一般的な要件です;)

これに基づいて、Pimpl 化されたクラスをいくらか簡単に定義できるようになりました。

class Foo
{
public:

private:
  struct Impl;
  pimpl<Impl> mImpl;
}; // class Foo

: コンパイラは、正しいコンストラクタ、コピー代入演算子、またはデストラクタをここで生成できません。これを行うと、Impl定義へのアクセスが必要になるためです。したがって、pimplヘルパーにもかかわらず、手動でこれら 4 を定義する必要があります。ただし、pimpl ヘルパーのおかげで、コンパイルは失敗し、未定義の動作に引きずり込まれることはありません。

3. さらに先へ

関数の存在は実装の詳細と見なされることが多いことに注意してくださいvirtual。Pimpl の利点の 1 つは、戦略パターンの力を活用するための適切なフレームワークがあることです。

そのためには、pimpl の「コピー」を変更する必要があります。

// pimpl.h
template <typename T>
pimpl<T>::pimpl(pimpl<T> const& rhs): m(rhs.m->clone()) {}

template <typename T>
pimpl<T>& pimpl<T>::operator=(pimpl<T> const& rhs)
{
  std::auto_ptr<T> tmp(rhs.m->clone()); // copy may throw: Strong Guarantee
  checked_delete(m);
  m = tmp.release();
  return *this;
}

そして、Foo好きなものを定義することができます

// foo.h
#include "pimpl.h"

namespace detail { class FooBase; }

class Foo
{
public:
  enum Mode {
    Easy,
    Normal,
    Hard,
    God
  };

  Foo(Mode mode);

  // Others

private:
  pimpl<detail::FooBase> mImpl;
};

// Foo.cpp
#include "foo.h"

#include "detail/fooEasy.h"
#include "detail/fooNormal.h"
#include "detail/fooHard.h"
#include "detail/fooGod.h"

Foo::Foo(Mode m): mImpl(FooFactory::Get(m)) {}

の ABI は、Foo発生する可能性のあるさまざまな変更にまったく関係がないことに注意してください。

  • 仮想メソッドはありませんFoo
  • のサイズはmImpl、それが何を指していても、単純なポインタのサイズです

したがって、クライアントは、メソッドまたは属性のいずれかを追加する特定のパッチについて心配する必要はなく、メモリ レイアウトなどについて心配する必要もありません。自然に機能します。

于 2010-08-30T08:40:22.857 に答える
5

PIMPL イディオムを使用すると、IMPL クラスの内部実装の詳細が変更された場合、クライアントを再構築する必要はありません。IMPL (およびヘッダー ファイル) クラスのインターフェイスを変更すると、明らかに PIMPL クラスを変更する必要があります。

ところで、示されているコードでは、IMPL と PIMPL の間に強い結合があります。そのため、IMPL のクラス実装を変更すると、再構築が必要になります。

于 2010-08-30T03:40:15.467 に答える
4

より現実的なものを検討すると、メリットがより顕著になります。コンパイラのファイアウォールと実装の非表示にこれを使用したほとんどの場合、可視クラスが含まれている同じコンパイル単位内で実装クラスを定義します。あなたの例では、Impl.horはなく、次のようになりますImpl.cppPimpl.cpp

#include <iostream>
#include <boost/thread.hpp>

class Impl {
public:
  Impl(): data(0) {}
  void setData(int d) {
    boost::lock_guard l(lock);
    data = d;
  }
  int getData() {
    boost::lock_guard l(lock);
    return data;
  }
  void doSomething() {
    int d = getData();
    std::cout << getData() << std::endl;
  }
private:
  int data;
  boost::mutex lock;
};

Pimpl::Pimpl(): pimpl(new Impl) {
}

void Pimpl::doSomething() {
  pimpl->doSomething();
}

これで、 への依存関係について誰も知る必要がなくなりましたboost。これは、ポリシーと組み合わせるとさらに強力になります。Implスレッド ポリシーなどの詳細 (たとえば、シングルかマルチか) は、舞台裏でさまざまな実装を使用することによって隠すことができます。Implまた、公開されていない、利用可能な追加のメソッドがいくつかあることにも注意してください。これにより、この手法は実装のレイヤー化にも適しています。

于 2010-08-30T03:58:28.420 に答える
3

あなたの例ではdata、クライアントを再コンパイルすることなく実装を変更できます。これは、PImpl 仲介者なしでは当てはまりません。同様に、の署名または名前を (ある程度まで) 変更することができImlp::DoSomething、クライアントは知る必要がありません。

一般に、宣言できるものprivate(デフォルト) またはprotectedinImplは、クライアントを再コンパイルせずに変更できます。

于 2010-08-30T03:46:32.480 に答える
1

Pimpl以外のクラス ヘッダーでは、.hpp ファイルはクラスのパブリック コンポーネントとプライベート コンポーネントをすべて 1 つの大きなバケットで定義します。

プライベートは実装と密接に結びついているため、.hpp ファイルは実際に内部実装について多くの情報を提供できることを意味します。

クラス内でプライベートに使用することを選択したスレッド ライブラリのようなものを検討してください。Pimpl を使用しないと、スレッド クラスと型がプライベート メンバーまたはプライベート メソッドのパラメーターとして検出される可能性があります。わかりました、スレッド ライブラリは悪い例かもしれませんが、考えはわかります: クラス定義のプライベート部分は、ヘッダーをインクルードする人から隠されている必要があります。

そこで Pimpl の出番です。 public クラス ヘッダーは「プライベート パーツ」を定義しなくなり、代わりにPointer to Implementationを持つため、パブリック クラス ヘッダーを「#include」するロジックからプライベートな世界が隠されます。

プライベート メソッド (実装) を変更すると、Pimpl の下に隠されているものが変更されるため、クラスのクライアントは再コンパイルする必要がありません。クライアントの観点からは何も変更されていないためです。プライベート実装メンバーは表示されなくなります。

http://www.gotw.ca/gotw/028.htm

于 2010-08-30T04:36:59.277 に答える
1

すべてのクラスが p-impl の恩恵を受けるわけではありません。あなたの例には、その内部状態にプリミティブ型しかありません。これは、明らかな利点がない理由を説明しています。

メンバーのいずれかが別のヘッダーで宣言された複合型を持っている場合、p-impl がそのヘッダーのインクルードをクラスのパブリック ヘッダーから実装ファイルに移動することがわかります。埋め込みフィールドでもスマート ポインターでもありません)。すべてのメンバー変数への生のポインターを個別に使用することもできますが、すべての状態への単一のポインターを使用すると、メモリ管理が容易になり、データの局所性が向上します (これらすべての型が順番に p-impl を使用する場合、局所性はあまりありません)。

于 2010-08-30T04:48:31.880 に答える