33

C ++でポリモーフィックな動作を実装する場合、純粋仮想メソッドを使用するか、関数ポインター(または関数)を使用できます。たとえば、非同期コールバックは次の方法で実装できます。

アプローチ1

class Callback
{
public:
    Callback();
    ~Callback();
    void go();
protected:
    virtual void doGo() = 0;  
};

//Constructor and Destructor

void Callback::go()
{
   doGo();
}

したがって、ここでコールバックを使用するには、doGo()メソッドをオーバーライドして、必要な関数を呼び出す必要があります。

アプローチ2

typedef void (CallbackFunction*)(void*)

class Callback
{
public:
    Callback(CallbackFunction* func, void* param);
    ~Callback();
    void go();
private:
   CallbackFunction* iFunc;
   void* iParam;
};

Callback::Callback(CallbackFunction* func, void* param) :
    iFunc(func),
    iParam(param)
{}

//Destructor

void go()
{
    (*iFunc)(iParam);
}

ここでコールバックメソッドを使用するには、Callbackオブジェクトによって呼び出される関数ポインターを作成する必要があります。

アプローチ3

[これは私(アンドレアス)によって質問に追加されました。元のポスターによって書かれたものではありません]

template <typename T>
class Callback
{
public:
    Callback() {}
    ~Callback() {}
    void go() {
        T t; t();
    }
};

class CallbackTest
{
public:
    void operator()() { cout << "Test"; }
};

int main()
{
    Callback<CallbackTest> test;

    test.go();
}

各実装の長所と短所は何ですか?

4

8 に答える 8

14

アプローチ 1 (仮想関数)

  • "+" "C++ でそれを行う正しい方法
  • "-" コールバックごとに新しいクラスを作成する必要があります
  • "-" 関数ポインターと比較して、VF テーブルを介したパフォーマンス上の追加の逆参照。Functor ソリューションと比較した 2 つの間接参照。

アプローチ 2 (関数ポインターを使用したクラス)

  • "+" C++ コールバック クラスの C スタイル関数をラップできます
  • "+" コールバック関数は、コールバック オブジェクトの作成後に変更できます
  • "-" 間接呼び出しが必要です。コンパイル時に静的に計算できるコールバックの functor メソッドよりも遅い場合があります。

アプローチ 3 (T ファンクターを呼び出すクラス)

  • "+" おそらく最速の方法です。間接的な呼び出しのオーバーヘッドはなく、完全にインライン化できます。
  • "-" 追加の Functor クラスを定義する必要があります。
  • "-" コンパイル時にコールバックを静的に宣言する必要があります。

FWIW、関数ポインターはファンクターと同じではありません。ファンクター (C++) は、通常は operator() である関数呼び出しを提供するために使用されるクラスです。

以下はファンクターの例と、ファンクター引数を利用するテンプレート関数です。

class TFunctor
{
public:
    void operator()(const char *charstring)
    {
        printf(charstring);
    }
};

template<class T> void CallFunctor(T& functor_arg,const char *charstring)
{
    functor_arg(charstring);
};

int main()
{
    TFunctor foo;
    CallFunctor(foo,"hello world\n");
}

パフォーマンスの観点から見ると、仮想関数と関数ポインターはどちらも間接的な関数呼び出し (つまり、レジスター経由) になりますが、仮想関数は関数ポインターをロードする前に VFTABLE ポインターを追加でロードする必要があります。ファンクターを (非仮想呼び出しで) コールバックとして使用することは、関数をテンプレート化するパラメーターを使用する最もパフォーマンスの高い方法です。これは、関数をインライン化することができ、インライン化されていなくても間接呼び出しを生成しないためです。

于 2009-12-23T20:26:07.180 に答える
7

アプローチ1

  • 読みやすく理解しやすい
  • エラーの可能性が少ない ( iFuncNULL にできない、 a を使用していないvoid *iParamなど)
  • C++ プログラマーは、これが C++ で行う「正しい」方法であると言うでしょう。

アプローチ 2

  • タイピングが少し減る
  • 非常にわずかに高速です(仮想メソッドの呼び出しにはオーバーヘッドがあり、通常は2つの単純な算術演算と同じです..したがって、ほとんど問題にはなりません)
  • それはあなたがCでそれを行う方法です

アプローチ 3

可能であれば、おそらくそれを行う最良の方法です。最高のパフォーマンスが得られ、タイプ セーフであり、理解しやすい (STL で使用される方法です)。

于 2009-12-23T20:26:40.360 に答える
5

アプローチ 2 の主な問題は、単にスケーリングできないことです。100 個の関数に相当するものを考えてみましょう。

class MahClass {
    // 100 pointers of various types
public:
    MahClass() { // set all 100 pointers }
    MahClass(const MahClass& other) {
        // copy all 100 function pointers
    }
};

MahClass のサイズは膨れ上がり、それを構築する時間も大幅に増加しました。ただし、仮想関数は、クラスのサイズが O(1) 増加し、それを構築する時間が増加します。言うまでもなく、ユーザーは、すべての派生クラスのすべてのコールバックを手動で記述して、ポインターを次のように調整する必要があります。派生へのポインターであり、関数ポインターの種類と混乱を指定する必要があります。1 つを忘れたり、NULL に設定したり、同じようにばかげたものに設定したりする可能性があるという考えは言うまでもありません。

アプローチ 3 は、目的のコールバックが静的に認識できる場合にのみ使用できます。

これにより、動的メソッド呼び出しが必要な場合に使用できる唯一のアプローチとしてアプローチ 1 が残されます。

于 2012-06-22T18:00:45.117 に答える
3

ユーティリティクラスを作成しているかどうかは、例からは明らかではありません。コールバッククラスは、クロージャを実装することを目的としていますか、それとも具体化していないより実質的なオブジェクトを実装することを目的としていますか?

最初の形式:

  • 読みやすく、理解しやすい、
  • 拡張がはるかに簡単です。pause、resumestopの各メソッドを追加してみてください。
  • カプセル化の処理に優れています(doGoがクラスで定義されていることを前提としています)。
  • おそらくより良い抽象化なので、保守が簡単です。

2番目の形式:

  • doGoのさまざまな方法で使用できるため、単なる多態性ではありません。
  • (追加のメソッドを使用して)実行時にdoGoメソッドを変更できるようにし、オブジェクトのインスタンスが作成後に機能を変更できるようにすることができます。

最終的に、IMO、最初の形式はすべての通常の場合に適しています。2つ目はいくつかの興味深い機能を備えていますが、頻繁に必要になる機能はありません。

于 2009-12-23T20:39:17.520 に答える
1

最初の方法の主な利点の 1 つは、型の安全性が高いことです。2 番目の方法では、iParam に void * を使用するため、コンパイラは型の問題を診断できません。

2 番目の方法の小さな利点は、C と統合する作業が少なくて済むことです。ただし、コード ベースが C++ のみの場合、この利点は意味がありません。

于 2009-12-23T20:36:49.803 に答える
0

たとえば、クラスに読み取り機能を追加するためのインターフェースを見てみましょう。

struct Read_Via_Inheritance
{
   virtual void  read_members(void) = 0;
};

別の読み取りソースを追加するときはいつでも、クラスから継承して特定のメソッドを追加する必要があります。

struct Read_Inherited_From_Cin
  : public Read_Via_Inheritance
{
  void read_members(void)
  {
    cin >> member;
  }
};

ファイル、データベース、またはUSBから読み取りたい場合は、さらに3つの個別のクラスが必要です。組み合わせは、複数のオブジェクトと複数のソースで非常に醜くなり始めます。

ビジターのデザインパターンに似ているファンクターを使用すると、次のようになります。

struct Reader_Visitor_Interface
{
  virtual void read(unsigned int& member) = 0;
  virtual void read(std::string& member) = 0;
};

struct Read_Client
{
   void read_members(Reader_Interface & reader)
   {
     reader.read(x);
     reader.read(text);
     return;
   }
   unsigned int x;
   std::string& text;
};

read_members上記の基盤により、オブジェクトは、メソッドにさまざまなリーダーを提供するだけで、さまざまなソースから読み取ることができます。

struct Read_From_Cin
  : Reader_Visitor_Interface
{
  void read(unsigned int& value)
  {
     cin>>value;
  }
  void read(std::string& value)
  {
     getline(cin, value);
  }
};

オブジェクトのコードを変更する必要はありません(すでに機能しているので良いことです)。リーダーを他のオブジェクトに適用することもできます。

一般的に、ジェネリックプログラミングを実行するときは継承を使用します。たとえば、Fieldクラスがある場合は、、、を作成Field_BooleanできField_TextますField_Integer。Inは、インスタンスへのポインタをに入れて、vector<Field *>それをレコードと呼ぶことができます。レコードはフィールドに対して一般的な操作を実行でき、処理されるフィールドの種類を気にしたり、認識したりしません。

于 2009-12-23T20:41:42.380 に答える
0

関数ポインターは、私が言うように、より C スタイルです。主な理由は、それらを使用するには、通常、ポインター定義とまったく同じシグネチャを持つフラット関数を定義する必要があるためです。

私が C++ を書くとき、私が書く唯一のフラット関数は int main() です。それ以外はすべてクラス オブジェクトです。2 つの選択肢から、クラスを定義して仮想をオーバーライドすることを選択しますが、クラスで何らかのアクションが発生したことをコードに通知することだけが必要な場合は、これらの選択肢のどちらも最適なソリューションにはなりません。

正確な状況はわかりませんが、デザインパターンを熟読することをお勧めします

オブザーバーパターンをお勧めします。クラスを監視したり、何らかの通知を待つ必要があるときに使用します。

于 2009-12-23T20:28:39.090 に答える
0
  1. まずはピュアバーチャルに変更。次に、インライン化します。インライン化が失敗しない限り、メソッドのオーバーヘッド呼び出しをまったく無効にする必要があります (強制しても失敗しません)。
  2. これは、C と比較して C++ の唯一の実際に役立つ主要な機能であるため、C を使用することもできます。常にメソッドを呼び出し、インライン化できないため、効率が低下します。
于 2009-12-23T22:14:19.710 に答える