5

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

我不认为仅使用包装器就可以轻松完成此操作,因为您必须拦截 IO 调用,因此包装类会将代码放在错误的层。本质上,您想替换对象下方的 IO 代码,但您正试图从顶层执行此操作。如果您将代码视为洋葱,那么您正在尝试修改外皮以影响两层或三层的东西;恕我直言,这表明该设计可能需要重新考虑。

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

于 2009-08-09T07:00:35.093 回答
1

它看起来像一个简单的任务,假设“NumberCruncher”有一个已知的接口,比如说 int operator(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);
   }
};
于 2009-08-09T09:47:46.113 回答
1

我想我有你正在寻找的答案,或者,至少,我几乎有。它使用了你建议的调度风格是愚蠢的,但我认为它符合你提出的前两个标准,或多或少符合第三个标准。

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

基本思想是创建一个模板类,其参数是要包装的对象的类,模板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++ 中实现成员函数委托的非常广泛的文章中得到了很多帮助。任何想要了解的比他们意识到的更多可能了解成员函数指针的人都应该好好阅读这篇文章。
于 2009-08-16T07:58:27.613 回答
0

我认为您需要的是代理/装饰器(设计模式)之类的东西。如果您不需要这些模式的动态部分,则可以使用模板。关键是你需要很好地定义你需要的接口。

于 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 回答