7

私は最近、Java と Ruby から C++ に切り替えましたが、驚いたことに、プライベート メソッドのメソッド シグネチャを変更すると、パブリック インターフェイスを使用するファイルを再コンパイルする必要があります。これは、プライベート部分も .h ファイルにあるためです。

私はすぐに、おそらく Java プログラマーにとって典型的な解決策、つまりインターフェース (= 純粋仮想基底クラス) を思いつきました。例えば:

BananaTree.h:

class Banana;

class BananaTree
{
public:
  virtual Banana* getBanana(std::string const& name) = 0;

  static BananaTree* create(std::string const& name);
};

BananaTree.cpp:

class BananaTreeImpl : public BananaTree
{
private:
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

public:
  BananaTreeImpl(string name) 
    : name(name)
  {}

  virtual Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }
};

BananaTree* BananaTree::create(string const& name)
{
  return new BananaTreeImpl(name);
}

ここでの唯一の問題は、 を使用できずnew、代わりに を呼び出さなければならないことBananaTree::create()です。とにかく工場をたくさん使うことを期待しているので、それは本当に問題ではないと思います。

しかし、C++ で有名な賢者たちは、別の解決策pImpl イディオムを思い付きました。それで、正しく理解できれば、私のコードは次のようになります。

BananaTree.h:

class BananaTree
{
public:
  Banana* addStep(std::string const& name);

private:
  struct Impl;
  shared_ptr<Impl> pimpl_;
};

BananaTree.cpp:

struct BananaTree::Impl
{
  string name;

  Banana* findBanana(string const& name)
  {
    return //obtain banana, somehow;
  }

  Banana* getBanana(string const& name)
  {
    return findBanana(name);
  }

  Impl(string const& name) : name(name) {}
}

BananaTree::BananaTree(string const& name)
  : pimpl_(shared_ptr<Impl>(new Impl(name)))
{}

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->getBanana(name);
}

これは、 のすべてのパブリック メソッド (BananaTreeこの場合は ) に対して、デコレータ スタイルの転送メソッドを実装する必要があることを意味しgetBananaます。これは、私が要求したくない複雑さとメンテナンスの労力の追加レベルのように思えます。

では、ここで質問です。純粋仮想クラスのアプローチの何が問題なのですか? pImpl アプローチのほうが文書化されているのはなぜですか? 何か見逃しましたか?

4

2 に答える 2

12

いくつかの違いを考えることができます:

仮想基本クラスを使用すると、正常に動作する C++ クラスに期待されるセマンティクスの一部を破ることができます。

次のように、クラスがスタック上でインスタンス化されることを期待します (または要求することさえあります)。

BananaTree myTree("somename");

そうしないと、RAII が失われ、手動で割り当ての追跡を開始する必要があり、多くの頭痛とメモリ リークが発生します。

また、クラスをコピーすることも期待しています。これを行うだけです

BananaTree tree2 = mytree;

もちろん、コピー コンストラクターを非公開にすることでコピーが禁止されている場合を除きます。この場合、その行はコンパイルすらされません。

上記のケースでは、インターフェイス クラスに実際には意味のあるコンストラクタがないという問題があることは明らかです。しかし、上記の例のようなコードを使用しようとすると、多くのスライスの問題にも遭遇します。ポリモーフィック オブジェクトでは、通常、スライスを防ぐために、オブジェクトへのポインターまたは参照を保持する必要があります。最初のポイントで述べたように、これは一般的に望ましくなく、メモリ管理をより困難にします。

あなたのコードの読者は、BananaTree基本的に a が機能しないこと、代わりにBananaTree*orを使用しなければならないことを理解できますか?BananaTree&

基本的に、あなたのインターフェイスは最新の C++ ではうまく機能しません。

  • ポインタはできるだけ避けてください。
  • 自動ライフタイム管理から利益を得るために、すべてのオブジェクトをスタック割り当てします。

ところで、仮想基底クラスは仮想デストラクタを忘れていました。それは明らかなバグです。

最後に、ボイラープレート コードの量を削減するために私がときどき使用する pimpl のより単純な変形は、「外部」オブジェクトに内部オブジェクトのデータ メンバーへのアクセスを与えることです。これにより、インターフェイスの重複を避けることができます。外側のオブジェクトの関数は、内側のオブジェクトから必要なデータに直接アクセスするか、内側のオブジェクトのヘルパー関数を呼び出しますが、これは外側のオブジェクトには同等のものはありません。

あなたの例では、関数 and を削除し、Impl::getBanana代わりにBananaTree::getBanana次のように実装できます。

Banana* BananaTree::getBanana(string const& name)
{
  return pimpl_->findBanana(name);
}

getBanana次に、1 つの関数 (BananaTreeクラス内) と 1 つのfindBanana関数 (クラス内)を実装するだけで済みますImpl

于 2010-06-22T11:17:57.990 に答える
1

実際、これは単なる設計上の決定です。また、「間違った」決定を下したとしても、切り替えるのはそれほど難しくありません。

pimpl は、スタック上の軽量オブジェクトを提供したり、同じ実装オブジェクトを参照して「コピー」を提示したりするためにも使用されます。
委任関数は面倒な場合がありますが、特にクラスが限られている場合は、小さな問題です (単純なので、実際に複雑になることはありません)。

C++ のインターフェイスは通常、実装を選択できることが期待される戦略のような方法で使用されますが、必須ではありません。

于 2010-06-22T11:16:20.477 に答える