我想我有你正在寻找的答案,或者,至少,我几乎有。它使用了你建议的调度风格是愚蠢的,但我认为它符合你提出的前两个标准,或多或少符合第三个标准。
- 包装类根本不需要修改。
- 它根本不修改包装的类。
- 它仅通过引入调度函数来更改语法。
基本思想是创建一个模板类,其参数是要包装的对象的类,模板dispatch
方法,其参数是成员函数的参数和返回类型。dispatch 方法查找传入的成员函数指针,看它之前是否被调用过。如果是这样,它会检索先前方法参数和计算结果的记录,以返回先前计算的分配给分派的参数的值,或者如果它是新的,则计算它。
由于这个包装类的作用也称为memoization,所以我选择调用模板Memo
,因为它比键入更短,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):
称为三(1)
3
称为三(2)
3
3
3
叫方(2.3)
5.29
5.29
笔记
- 这仅适用于采用 1 个参数并返回结果的方法。它不适用于采用 0 个参数、2 个、3 个或更多参数的方法。不过,这应该不是什么大问题。您可以实现分派的重载版本,该版本采用不同数量的参数,直至某个合理的最大值。这就是Boost Tuple 库所做的。他们实现了最多 10 个元素的元组,并假设大多数程序员不需要更多。
- 为调度实现多个重载的可能性是我使用 FirstEq 谓词模板和 find_if 算法而不是简单的 for 循环搜索的原因。单次使用的代码要多一些,但是如果您要多次进行类似的搜索,最终会减少整体代码,并且使其中一个循环出现细微错误的机会也更少。
- 它不适用于不返回任何内容的方法,即 ie
void
,但如果该方法不返回任何内容,那么您不需要缓存结果!
- 它不适用于包装类的模板成员函数,因为您需要将实际的成员函数指针传递给调度,而未实例化的模板函数(还)没有指针。可能有办法解决这个问题,但我还没有尝试太多。
- 我还没有对此进行太多测试,因此它可能存在一些微妙(或不那么微妙)的问题。
- 我认为在 C++ 中不可能有一个完全无缝的解决方案,它可以完全满足您的所有要求而语法没有任何变化。(尽管我很想被证明是错误的!)希望这已经足够接近了。
- 当我研究这个答案时,我从这篇关于在 C++ 中实现成员函数委托的非常广泛的文章中得到了很多帮助。任何想要了解的比他们意识到的更多可能了解成员函数指针的人都应该好好阅读这篇文章。