私はあなたが求めている答えを持っていると思います、または少なくとも、ほとんどそうです. それはあなたが提案したディスパッチ スタイルを使用していますが、あなたが設定した最初の 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++ でのメンバー関数デリゲートの実装に関するこの非常に広範な記事から多くの助けを得ました。メンバー関数ポインターについて知ることができると思っていた以上のことを知りたい人は、その記事をよく読んでください。