5

ラップされたクラスと直接やり取りせずに、汎用クラスに機能を追加するクラスを作成しようとしています。この良い例は、スマート ポインターです。具体的には、ラッパーを介して呼び出された 1 つの (または任意の) メソッドのすべての i/o をキャッシュするラッパーを作成したいと考えています。理想的には、キャッシュ ラッパーには次のプロパティがあります。

  • ラッピング クラスを何らかの方法で変更する必要はありません (つまり、ジェネリック)。
  • ラップされたクラスを何らかの方法で変更する必要はありません (つまり、ジェネリック)。
  • オブジェクトを使用するためのインターフェースや構文を大幅に変更することはありません

たとえば、次のように使用すると非常に便利です。

CacheWrapper<NumberCruncher> crunchy;
...
// do some long and ugly calculation, caching method input/output
result = crunchy->calculate(input); 
...
// no calculation, use cached result
result = crunchy->calculate(input); 

このような間抜けなものは問題ありませんが:

result = crunchy.dispatch (&NumberCruncher::calculate, input);

これは C++ で可能であるべきだと思いますが、おそらくどこかで構文上の体操が必要になるでしょう。

何か案は?

4

5 に答える 5

1

It looks like a simple task, assuming the "NumberCruncher" has a known interface, let's say int operator(int). Note that you'll need to make it more complicated to support other interfaces. In order to do so, i'm adding another template parameter, an Adaptor. Adaptor should convert some interface to a known interface. Here's simple and dumb implementation with static method, which is one way to do it. Also look what Functor is.

struct Adaptor1 {
     static int invoke(Cached1 & c, int input)  {
         return(c.foo1(input));
     }
};

struct Adaptor2 {
     static int invoke(Cached2 & c, int input)  {
         return(c.foo2(input));
     }
};

template class CacheWrapper<typename T, typeneame Adaptor>
{
private:
  T m_cachedObj;
  std::map<int, int> m_cache;

public:
   // add c'tor here

   int calculate(int input) {
      std::map<int, int>::const_iterator it = m_cache.find(input);
      if (it != m_cache.end()) {
         return(it->second);
      }
      int res = Adaptor::invoke(m_cachedObj, input);
      m_cache[input] = res;
      return(res);
   }
};
于 2009-08-09T09:47:46.113 に答える
1

私はあなたが求めている答えを持っていると思います、または少なくとも、ほとんどそうです. それはあなたが提案したディスパッチ スタイルを使用していますが、あなたが設定した最初の 2 つの基準を満たし、3 番目の基準を多かれ少なかれ満たしていると思います。

  1. ラッピング クラスを変更する必要はまったくありません。
  2. ラップされたクラスはまったく変更されません。
  3. ディスパッチ関数を導入して構文を変更するだけです。

dispatch基本的な考え方は、パラメーターがメンバー関数の引数と戻り値の型であるテンプレート メソッドを使用して、パラメーターがラップされるオブジェクトのクラスであるテンプレート クラスを作成することです。ディスパッチ メソッドは、渡されたメンバ関数ポインタを調べて、それが以前に呼び出されたかどうかを確認します。その場合、以前のメソッド引数と計算結果のレコードを取得して、ディスパッチに指定された引数に対して以前に計算された値を返すか、新しい場合は計算します。

このラッピング クラスが行うことはmemoizationMemoとも呼ばれるため、テンプレートを呼び出すことを選択しましたCacheWrapper

#include <algorithm>
#include <map>
#include <utility>
#include <vector>

// An anonymous namespace to hold a search predicate definition. Users of
// Memo don't need to know this implementation detail, so I keep it
// anonymous. I use a predicate to search a vector of pairs instead of a
// simple map because a map requires that operator< be defined for its key
// type, and operator< isn't defined for member function pointers, but
// operator== is.
namespace {
    template <typename Type1, typename Type2>
    class FirstEq {
        FirstType value;

    public:
        typedef std::pair<Type1, Type2> ArgType;

        FirstEq(Type1 t) : value(t) {}

        bool operator()(const ArgType& rhs) const { 
            return value == rhs.first;
        }
    };
};

template <typename T>
class Memo {
    // Typedef for a member function of T. The C++ standard allows casting a
    // member function of a class with one signature to a type of another
    // member function of the class with a possibly different signature. You
    // aren't guaranteed to be able to call the member function after
    // casting, but you can use the pointer for comparisons, which is all we
    // need to do.
    typedef void (T::*TMemFun)(void);

    typedef std::vector< std::pair<TMemFun, void*> > FuncRecords;

    T           memoized;
    FuncRecords funcCalls;

public:
    Memo(T t) : memoized(t) {}

    template <typename ReturnType, typename ArgType>
    ReturnType dispatch(ReturnType (T::* memFun)(ArgType), ArgType arg) {

        typedef std::map<ArgType, ReturnType> Record;

        // Look up memFun in the record of previously invoked member
        // functions. If this is the first invocation, create a new record.
        typename FuncRecords::iterator recIter = 
            find_if(funcCalls.begin(),
                    funcCalls.end(),
                    FirstEq<TMemFun, void*>(
                        reinterpret_cast<TMemFun>(memFun)));

        if (recIter == funcCalls.end()) {
            funcCalls.push_back(
                std::make_pair(reinterpret_cast<TMemFun>(memFun),
                               static_cast<void*>(new Record)));
            recIter = --funcCalls.end();
        }

        // Get the record of previous arguments and return values.
        // Find the previously calculated value, or calculate it if
        // necessary.
        Record*                   rec      = static_cast<Record*>(
                                                 recIter->second);
        typename Record::iterator callIter = rec->lower_bound(arg);

        if (callIter == rec->end() || callIter->first != arg) {
            callIter = rec->insert(callIter,
                                   std::make_pair(arg,
                                                  (memoized.*memFun)(arg)));
        }
        return callIter->second;
    }
};

以下は、その使用法を示す簡単なテストです。

#include <iostream>
#include <sstream>
#include "Memo.h"

using namespace std;

struct C {
    int three(int x) { 
        cout << "Called three(" << x << ")" << endl;
        return 3;
    }

    double square(float x) {
        cout << "Called square(" << x << ")" << endl;
        return x * x;
    }
};

int main(void) {
    C       c;
    Memo<C> m(c);

    cout << m.dispatch(&C::three, 1) << endl;
    cout << m.dispatch(&C::three, 2) << endl;
    cout << m.dispatch(&C::three, 1) << endl;
    cout << m.dispatch(&C::three, 2) << endl;

    cout << m.dispatch(&C::square, 2.3f) << endl;
    cout << m.dispatch(&C::square, 2.3f) << endl;

    return 0;
}

私のシステム(g ++ 4.0.1を使用するMacOS 10.4.11)で次の出力が生成されます。

three(1) と呼ばれる
3
three(2) と呼ばれる
3
3
3
スクエア(2.3)と呼ばれる
5.29
5.29

ノート

  • これは、1 つの引数を取り、結果を返すメソッドに対してのみ機能します。引数が 0 個、または 2 個、3 個、またはそれ以上のメソッドでは機能しません。ただし、これは大きな問題ではないはずです。いくつかの合理的な最大値まで異なる数の引数を取る、ディスパッチのオーバーロードされたバージョンを実装できます。これがBoost Tuple ライブラリの機能です。彼らは最大 10 個の要素のタプルを実装しており、ほとんどのプログラマーはそれ以上の要素を必要としないと想定しています。
  • ディスパッチ用に複数のオーバーロードを実装できる可能性があるため、単純な for ループ検索ではなく、find_if アルゴリズムで FirstEq 述語テンプレートを使用しました。1回の使用ではコードが少し増えますが、同様の検索を複数回行う場合は、全体としてコードが少なくなり、ループの1つが微妙に間違っている可能性が少なくなります.
  • 何も返さないメソッド、つまりvoidでは機能しませんが、メソッドが何も返さない場合は、結果をキャッシュする必要はありません!
  • 実際のメンバー関数ポインターをディスパッチに渡す必要があり、インスタンス化されていないテンプレート関数には (まだ) ポインターがないため、ラップされたクラスのテンプレート メンバー関数では機能しません。これを回避する方法があるかもしれませんが、私はまだあまり試していません。
  • 私はまだこれについて多くのテストを行っていないので、いくつかの微妙な (またはそれほど微妙ではない) 問題がある可能性があります。
  • 構文をまったく変更せずにすべての要件を満たす完全にシームレスなソリューションは、C++ では不可能だと思います。(私が間違っていることを証明したいのですが!) うまくいけば、これで十分です。
  • この回答を調査したとき、C++ でのメンバー関数デリゲートの実装に関するこの非常に広範な記事から多くの助けを得ました。メンバー関数ポインターについて知ることができると思っていた以上のことを知りたい人は、その記事をよく読んでください。
于 2009-08-16T07:58:27.613 に答える
0

I think what you need is something like a proxy / decorator (design patterns). You can use templates if you don't need the dynamic part of those patterns. The point is that you need to well define the interface that you will need.

于 2009-08-09T10:01:42.170 に答える
0

オブジェクト メソッドを処理するケースはまだわかりませんが、通常の関数についてはうまく修正できたと思います。

template <typename input_t, typename output_t>
class CacheWrapper
{
public:
  CacheWrapper (boost::function<output_t (input_t)> f)
    : _func(f)
  {}

  output_t operator() (const input_t& in)
  {
    if (in != input_)
      {
        input_ = in;
        output_ = _func(in);
      }
    return output_;
  }

private:
  boost::function<output_t (input_t)> _func;
  input_t input_;
  output_t output_;
};

これは次のように使用されます。

#include <iostream>
#include "CacheWrapper.h"

double squareit(double x) 
{ 
  std::cout << "computing" << std::endl;
  return x*x;
}

int main (int argc, char** argv)
{
  CacheWrapper<double,double> cached_squareit(squareit);

  for (int i=0; i<10; i++)
    {
      std::cout << cached_squareit (10) << std::endl;
    }
}

これをオブジェクトに対して機能させる方法に関するヒントはありますか?

于 2009-08-10T23:00:00.027 に答える