22

比較した2つの手法の長所/短所は何ですか?そしてもっと重要なのは、なぜ、いつ一方を他方よりも使用する必要があるのか​​ということです。それは個人的な好み/好みの問題ですか?

私の能力の限りでは、私の質問に明示的に対処する別の投稿は見つかりませんでした。ポリモーフィズムおよび/または型消去の実際の使用に関する多くの質問の中で、以下が最も近いように思われるか、そう思われましたが、それは私の質問にも実際には対応していません。

C ++-&CRTP。型消去とポリモーフィズム

私は両方のテクニックをよく理解していることに注意してください。この目的のために、以下に簡単で自己完結型の実用的な例を示します。これは、不要と思われる場合は削除できます。ただし、この例では、私の質問に関して2つの手法が何を意味するのかを明確にする必要があります。私は命名法について議論することに興味がありません。また、コンパイル時と実行時のポリモーフィズムの違いは知っていますが、これは質問に関連しているとは思いません。パフォーマンスの違いがある場合は、パフォーマンスの違いにはあまり関心がないことに注意してください。しかし、パフォーマンスに基づいてどちらか一方に印象的な議論があった場合、私はそれを読みたいと思います。特に、2つのアプローチのいずれかでのみ実際に機能する具体的な例(コードなし)について聞きたいと思います。

以下の例を見ると、主な違いの1つはメモリ管理です。これは、ポリモーフィズムの場合はユーザー側に残り、型消去の場合は参照カウント(またはブースト)が必要になります。とは言っても、使用シナリオによっては、ポリモーフィズムの状況が改善される可能性があります。たとえば、ベクトル(?)でスマートポインターを使用することで状況が改善される可能性がありますが、任意の場合、これは実用的ではないことがよくあります(?)。型消去を支持する可能性のある別の側面は、共通のインターフェースの独立性かもしれませんが、なぜそれが利点になるのでしょうか(?)。

以下に示すコードは、以下のすべてのコードブロックを単一のソースファイルに入れるだけで、MS VisualStudio 2008でテスト(コンパイルおよび実行)されました。Linuxではgccでもコンパイルする必要があります。そうしないと、理由がわからないので(?):-)わかりやすくするために、ここでコードを分割/分割しました。

これらのヘッダーファイルで十分ですよね(?)。

#include <iostream>
#include <vector>
#include <string>

ブースト(または他の)依存関係を回避するための単純な参照カウント。このクラスは、以下のtype-erasure-exampleでのみ使用されます。

class RefCount
{
  RefCount( const RefCount& );
  RefCount& operator= ( const RefCount& );
  int m_refCount;

  public:
    RefCount() : m_refCount(1) {}
    void Increment() { ++m_refCount; }
    int Decrement() { return --m_refCount; }
};

これは単純な型消去の例/図です。次の記事から一部コピーおよび変更されました。主に私はそれをできるだけ明確でわかりやすくするように努めました。 http://www.cplusplus.com/articles/oz18T05o/

class Object {
  struct ObjectInterface {
    virtual ~ObjectInterface() {}
    virtual std::string GetSomeText() const = 0;
  };

  template< typename T > struct ObjectModel : ObjectInterface {
    ObjectModel( const T& t ) : m_object( t ) {}
    virtual ~ObjectModel() {}
    virtual std::string GetSomeText() const { return m_object.GetSomeText(); }
    T m_object;
 };

  void DecrementRefCount() {
    if( mp_refCount->Decrement()==0 ) {
      delete mp_refCount; delete mp_objectInterface;
      mp_refCount = NULL; mp_objectInterface = NULL;
    }
  }

  Object& operator= ( const Object& );
  ObjectInterface *mp_objectInterface;
  RefCount *mp_refCount;

  public:
    template< typename T > Object( const T& obj )
      : mp_objectInterface( new ObjectModel<T>( obj ) ), mp_refCount( new RefCount ) {}
    ~Object() { DecrementRefCount(); }

    std::string GetSomeText() const { return mp_objectInterface->GetSomeText(); }

    Object( const Object &obj ) {
      obj.mp_refCount->Increment(); mp_refCount = obj.mp_refCount;
      mp_objectInterface = obj.mp_objectInterface;
    }
};

struct MyObject1 { std::string GetSomeText() const { return "MyObject1"; } };
struct MyObject2 { std::string GetSomeText() const { return "MyObject2"; } };

void UseTypeErasure() {
  typedef std::vector<Object> ObjVect;
  typedef ObjVect::const_iterator ObjVectIter;

  ObjVect objVect;
  objVect.push_back( Object( MyObject1() ) );
  objVect.push_back( Object( MyObject2() ) );

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    std::cout << iter->GetSomeText();
}

私に関する限り、これはポリモーフィズムを使用してもほぼ同じことを達成しているようですが、そうではないかもしれません(?)。

struct ObjectInterface {
  virtual ~ObjectInterface() {}
  virtual std::string GetSomeText() const = 0;
};

struct MyObject3 : public ObjectInterface {
  std::string GetSomeText() const { return "MyObject3"; } };

struct MyObject4 : public ObjectInterface {
  std::string GetSomeText() const { return "MyObject4"; } };

void UsePolymorphism() {
  typedef std::vector<ObjectInterface*> ObjVect;
  typedef ObjVect::const_iterator ObjVectIter;

  ObjVect objVect;
  objVect.push_back( new MyObject3 );
  objVect.push_back( new MyObject4 );

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    std::cout << (*iter)->GetSomeText();

  for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter )
    delete *iter;
}

そして最後に、上記のすべてを一緒にテストします。

int main() {
  UseTypeErasure();
  UsePolymorphism();
  return(0);
}
4

3 に答える 3

8

C ++スタイルの仮想メソッドベースのポリモーフィズム:

  1. データを保持するには、クラスを使用する必要があります。
  2. すべてのクラスは、特定の種類のポリモーフィズムを念頭に置いて構築する必要があります。
  3. すべてのクラスには共通のバイナリレベルの依存関係があり、コンパイラが各クラスのインスタンスを作成する方法を制限します。
  4. 抽象化するデータは、ニーズを説明するインターフェースを明示的に説明する必要があります。

C ++スタイルのテンプレートベースの型消去(仮想メソッドベースのポリモーフィズムで消去を実行):

  1. あなたはあなたのデータについて話すためにテンプレートを使わなければなりません。
  2. 作業中のデータの各チャンクは、他のオプションとは完全に無関係である可能性があります。
  3. 型消去作業は、コンパイル時間を肥大化させるパブリックヘッダーファイル内で行われます。
  4. 消去された各タイプには、バイナリサイズを肥大化させる可能性のある独自のテンプレートがインスタンス化されています。
  5. 抽象化するデータは、ニーズに直接依存するものとして記述する必要はありません。

さて、どちらが良いですか?さて、それはあなたの特定の状況で上記のものが良いか悪いかによって異なります。

明示的な例として、std::function<...>関数ポインター、関数参照、コンパイル時に型を生成するテンプレートベースの関数の山全体の出力、operator()を持つ関数のmyraids、およびラムダを取得できる型消去を使用します。これらのタイプはすべて互いに無関係です。そして、それらはを持っていることに縛られていないので、それらが文脈のvirtual operator()外で使用されるとき、それらがstd::function表す抽象化はコンパイルすることができます。型消去なしではこれを行うことはできず、おそらくそうしたくないでしょう。

一方、クラスにと呼ばれるメソッドがあるからDoFooといって、それらがすべて同じことをするという意味ではありません。ポリモーフィズムでは、それはDoFooあなたが呼んでいるものだけではなくDoFoo、特定のインターフェースからのものです。

サンプルコードに関しては...あなたはポリモーフィズムの場合にあるGetSomeTextはずです。virtual ... override

型消去を使用しているという理由だけでカウントを参照する必要はありません。polymorphsmを使用しているという理由だけで、参照カウントを使用する必要はありません。

他の場合のrawポインタの保存方法と同じように、内容を手動で破棄してObjectラップすることができます(deleteを呼び出す必要があるのと同じです)。をラップすることができ、他の場合はをラップすることができます。を含めることができます。これは、他の場合のベクトルを持つのと同じです。のは、からコピーコンストラクタと代入演算子を抽出し、それらをに公開して、ポリモーフィズムの場合ののに対応する、の完全な値のセマンティクスを可能にします。T*vectorObjectstd::shared_ptr<T>vectorstd::shared_ptr<T>Objectstd::unique_ptr<T>std::unique_ptr<T>ObjectObjectModelTObjectObjectvectorT

于 2012-11-09T16:27:57.290 に答える
5

ここに 1 つのビューがあります。質問は、レイト バインディング (「ランタイム ポリモーフィズム」) とアーリー バインディング (「コンパイル時ポリモーフィズム」) のどちらを選択するかを尋ねているようです。

KerrekSB がコメントで指摘しているように、早期バインディングでは現実的ではないことを、遅延バインディングで実行できることがいくつかあります。Strategy パターン (ネットワーク I/O のデコード) または Abstract Factory パターン (実行時に選択されるクラス ファクトリ) の多くの用途は、このカテゴリに分類されます。

両方のアプローチが実行可能である場合、選択は関係するトレードオフの問題です。C++ アプリケーションでは、アーリー バインディングとレイト バインディングの間に見られる主なトレードオフは、実装の保守性、バイナリ サイズ、およびパフォーマンスです。

C++ のテンプレートは、どのような形であっても理解できないと感じている人が少なくとも何人かはいます。または、テンプレートに関して、それほど劇的ではない他の予約を行うこともできます。C++ テンプレートには、多くの小さな落とし穴 (「いつ 'typename' および 'template' キーワードを使用する必要があるか?」) と、自明ではないトリック (SFINAE が思い浮かびます) があります。

もう 1 つのトレードオフは最適化です。早期にバインドすると、プログラムに関するより多くの情報がコンパイラに提供されるため、(潜在的に) より適切な最適化を行うことができます。後でバインドすると、コンパイラーは (おそらく) 事前に多くの情報を認識しません。その情報の一部は他のコンパイル単位にある可能性があるため、オプティマイザーはそれほど多くのことを実行できません。

もう 1 つのトレードオフは、プログラムのサイズです。少なくとも C++ では、「コンパイル時のポリモーフィズム」を使用すると、使用される特殊化ごとにコンパイラが異なるコードを作成、最適化、および発行するため、バイナリ サイズが膨らむことがあります。対照的に、遅延バインディングの場合、コード パスは 1 つだけです。

異なるコンテキストで行われている同じトレードオフを比較するのは興味深いことです。ブラウザー間の違いに対処するために (ある種の) ポリモーフィズムを使用し、場合によっては国際化 (i18n)/ローカリゼーションを行う Web アプリケーションを考えてみましょう。ここで、手書きの JavaScript Web アプリケーションは、実行時に機能を検出して何をすべきかを判断するメソッドを持つことで、ここでレイト バインディングに相当するものを使用する可能性があります。jQuery のようなライブラリは、この手法を採用しています。

もう 1 つのアプローチは、考えられるブラウザ/i18n の可能性ごとに異なるコードを記述することです。これはばかげているように聞こえますが、前例のないことではありません。Google Web Toolkit はこのアプローチを使用します。GWT には、コンパイラの出力をさまざまなブラウザーやさまざまなローカリゼーションに特化するために使用される「遅延バインディング」メカニズムがあります。GWT の「据え置きバインディング」メカニズムは早期バインディングを使用します。GWT の Java-to-JavaScript コンパイラーは、ポリモーフィズムが必要になる可能性のあるすべての方法を見つけ出し、それぞれに対してまったく異なる「バイナリー」を吐き出します。

トレードオフは似ています。遅延バインディングを使用して GWT を拡張する方法に頭を悩ませるのは、頭痛の種です。コンパイル時に知識があると、GWT のコンパイラーは各特殊化を個別に最適化できるため、パフォーマンスが向上し、各特殊化のサイズが小さくなる可能性があります。GWT アプリケーション全体は、コンパイル済みのすべての特殊化により、同等の jQuery アプリケーションの何倍ものサイズになる可能性があります。

于 2012-11-09T15:32:43.573 に答える