通用非侵入式缓存包装器

时间:2009-08-09 05:47:03

标签: c++ generics templates metaprogramming memoization

我正在尝试创建一个类,它为泛型类添加功能,而不直接与包装类接口。一个很好的例子是智能指针。具体来说,我想创建一个包装器来缓存通过包装器调用的一个(或任何?)方法的所有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 ++中实现,尽管可能在某个地方有一些句法体操。

有什么想法吗?

5 个答案:

答案 0 :(得分:1)

我不认为只使用包装器就可以轻松完成,因为您必须拦截IO调用,因此包装类会将代码置于错误的层。实质上,您希望替换对象下面的IO代码,但是您尝试从顶层执行此操作。如果您将代码视为洋葱,那么您正在尝试修改外部皮肤以影响两三层的内容;恕我直言,建议设计可能需要重新考虑。

如果你试图以这种方式包装/修改的类允许你传入流(或者你使用的任何IO机制),那么用那个代替一个缓存就可以了;从本质上讲,这也是你试图用你的包装器实现的目标。

答案 1 :(得分:1)

看起来这是一个简单的任务,假设“NumberCruncher”有一个已知的接口,让我们说int运算符(int)。 请注意,您需要使其更复杂以支持其他接口。为了做到这一点,我正在添加另一个模板参数,一个适配器。适配器应将某些接口转换为已知接口。这是使用静态方法的简单和愚蠢的实现,这是一种方法。另外看看Functor是什么。

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);
   }
};

答案 2 :(得分:1)

我想我有你想要的答案,或者至少我差不多了。它使用您建议的发送方式很傻,但我认为它符合您提出的前两个标准,并且或多或少符合第三个标准。

  1. 根本不需要修改包装类。
  2. 根本不会修改包装类。
  3. 它只会通过引入调度函数来更改语法。
  4. 基本思想是创建一个模板类,其参数是要包装的对象的类,使用模板dispatch方法,其参数是成员函数的参数和返回类型。 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):

    Called three(1)
    3
    Called three(2)
    3
    3
    3
    Called square(2.3)
    5.29
    5.29
    

    注意

    • 这仅适用于采用1参数并返回结果的方法。它不适用于采用0个参数,或2个或3个或更多参数的方法。不过,这应该不是一个大问题。您可以实现dispatch的重载版本,它可以使用不同数量的参数,最多可达到一些合理的最大值。这就是Boost Tuple library的作用。它们实现了多达10个元素的元组,并假设大多数程序员不需要更多元素。
    • 为调度实现多个重载的可能性是我使用findEif算法而不是简单的for循环搜索的FirstEq谓词模板的原因。它只是一次性使用的代码,但是如果你要多次进行类似的搜索,最终会减少代码总量,减少使其中一个循环出现微妙错误的机会。
    • 它不适用于什么都不返回的方法,即void,但如果方法没有返回任何内容,那么你不需要缓存结果!
    • 它不适用于包装类的模板成员函数,因为您需要将实际的成员函数指针传递给dispatch,而未实例化的模板函数没有指针(尚未)。可能有办法解决这个问题,但我还没有尝试过。
    • 我还没有做太多测试,所以它可能有一些微妙(或不那么微妙)的问题。
    • 在C ++中,我认为完全无缝的解决方案可以满足您的所有要求而不需要改变语法。 (虽然我很想被证明是错的!)希望这足够接近。
    • 当我研究这个答案时,我从this very extensive write up获得了很多关于在C ++中实现成员函数委托的帮助。任何想要学习比他们意识到的方式更多的人都可以了解成员函数指针应该给那篇文章一个很好的阅读。

答案 3 :(得分:0)

我认为您需要的是像proxy / decorator(设计模式)。如果您不需要这些模式的动态部分,则可以使用模板。关键是你需要很好地定义你需要的接口。

答案 4 :(得分: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;
    }
}

有关如何使其适用于对象的任何提示吗?