代码之家  ›  专栏  ›  技术社区  ›  Andrew Prock

通用非入侵缓存包装器

  •  5
  • Andrew Prock  · 技术社区  · 15 年前

    我正在尝试创建一个类,该类向泛型类添加功能,而不直接与包装类接口。智能指针就是一个很好的例子。具体来说,我想创建一个包装器,它缓存通过包装器调用的一个(或任何?)方法的所有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);
    

    有什么想法吗?

    5 回复  |  直到 15 年前
        1
  •  1
  •   Timo Geusch    15 年前

    我认为仅仅使用包装器不容易做到这一点,因为您必须拦截IO调用,因此包装类会将代码放在错误的层。本质上,您希望替换对象下面的IO代码,但您正试图从顶层执行此操作。如果您将代码看作洋葱,那么您正试图修改外层皮肤,以影响两层或三层的内容;这表明设计可能需要重新考虑。

    如果您试图以这种方式包装/修改的类确实允许您传入流(或您使用的任何IO机制),那么将该类替换为缓存类将是正确的做法;从本质上讲,这也是您试图用包装器实现的目标。

        2
  •  1
  •   Drakosha    15 年前

    它看起来像一个简单的任务,假设“NumberCruncher”有一个已知的接口,比如int操作符(int)。 请注意,您需要使其更加复杂,以支持其他接口。为此,我添加了另一个模板参数,适配器。适配器应将某些接口转换为已知接口。下面是一个简单而愚蠢的静态方法实现,这是实现它的一种方法。再看看函子是什么。

    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);
       }
    };
    
        3
  •  1
  •   A. Levy    14 年前

    我想我找到了你想要的答案,或者,至少,我几乎找到了。它使用了您建议的发送样式,但我认为它符合您提出的前两个标准,并且或多或少符合第三个标准。

    1. 包装类根本不需要修改。
    2. 它根本不修改包装的类。
    3. 它只通过引入分派函数来更改语法。

    基本思想是使用模板创建一个模板类,其参数是要包装的对象的类 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;
    }
    

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

    • Boost Tuple library 做他们实现了最多10个元素的元组,并假设大多数程序员不需要更多的元素。
    • 为分派实现多个重载的可能性就是为什么我使用FirstEq谓词模板和find_if算法,而不是简单的for循环搜索。一次使用的代码要多一点,但是如果您要多次执行类似的搜索,那么最终的结果是代码总量会减少,并且获得一个细微错误的循环的机会也会减少。
    • 对于不返回任何内容的方法,即。 void ,但如果该方法不返回任何内容,则不需要缓存结果!
    • 它不适用于包装类的模板成员函数,因为您需要将实际的成员函数指针传递给dispatch,而未实例化的模板函数还没有指针。也许有办法解决这个问题,但我还没有试过很多。
    • 我认为没有一个完全无缝的解决方案,完全满足您的要求,语法完全没有变化,在C++中是可能的。(尽管我希望被证明是错的!)希望这已经足够接近了。
    • 当我研究这个答案时,我得到了来自 this very extensive write up
        4
  •  0
  •   neuro    15 年前

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

        5
  •  0
  •   Andrew Prock    15 年前

    我还没有弄清楚处理对象方法的理由,但我认为我已经找到了一个很好的解决常规函数的方法

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

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