универсальная неинвазивная оболочка кэша - PullRequest
5 голосов
/ 09 августа 2009

Я пытаюсь создать класс, который добавляет функциональность к универсальному классу, без непосредственного взаимодействия с обернутым классом. Хорошим примером этого будет умный указатель. В частности, я хотел бы создать оболочку, которая кэширует все операции ввода-вывода для одного (или любого?) Метода, вызываемого через оболочку. В идеале оболочка кэша имеет следующие свойства:

  • это не потребует какого-либо изменения класса обертки (т. Е. Универсального)
  • это не потребует какого-либо изменения обернутого класса (т. Е. Универсального)
  • это не изменит интерфейс или синтаксис для значительного использования объекта

Например, было бы неплохо использовать это так:

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 ]

1 голос
/ 16 августа 2009

Я думаю, что у меня есть ответ, который вы ищете, или, по крайней мере, я почти знаю. Он использует стиль отправки, который вы предложили, был глупым, но я думаю, что он соответствует первым двум критериям, которые вы изложили, и более или менее соответствует третьему.

  1. Класс упаковки вообще не нужно изменять.
  2. Он не изменяет обернутый класс вообще.
  3. Изменяет только синтаксис, вводя функцию диспетчеризации.

Основная идея состоит в том, чтобы создать шаблонный класс, параметром которого является класс объекта, который нужно обернуть, с помощью шаблона dispatch метода, параметры которого являются аргументом и возвращаемыми типами функции-члена. Метод dispatch просматривает переданный указатель на функцию-член, чтобы увидеть, был ли он вызван ранее. Если это так, он извлекает запись аргументов предыдущего метода и вычисленных результатов, чтобы вернуть ранее вычисленное значение для аргумента, переданного диспетчеру, или вычислить его, если оно новое.

Поскольку то, что делает этот класс-обертка, также называется памятка , я решил назвать шаблон 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;
}

, который выдает следующий вывод в моей системе (MacOS 10.4.11 с использованием g ++ 4.0.1):

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

ПРИМЕЧАНИЯ

  • Это работает только для методов, которые принимают 1 аргумент и возвращают результат. Это не работает для методов, которые принимают 0 аргументов, или 2, или 3, или больше аргументов. Это не должно быть большой проблемой, хотя. Вы можете реализовать перегруженные версии диспетчеризации, которые принимают разное количество аргументов вплоть до некоторого разумного максимума. Это то, что делает Boost Tuple библиотека . Они реализуют кортежи из 10 элементов и предполагают, что большинству программистов не требуется больше этого.
  • Возможность реализации нескольких перегрузок для диспетчеризации - вот почему я использовал шаблон предиката FirstEq с алгоритмом find_if вместо простого поиска в цикле. Это немного больше кода для однократного использования, но если вы собираетесь выполнять подобный поиск несколько раз, то в итоге будет меньше кода в целом и меньше шансов сделать один из циклов слегка ошибочным.
  • Это не работает для методов, которые ничего не возвращают, т.е. void, но если метод ничего не возвращает, вам не нужно кэшировать результат!
  • Это не работает для функций-членов шаблона обернутого класса, потому что вам нужно передать фактический указатель на функцию-член в dispatch, а неинстанцированная функция шаблона не имеет указателя (пока). Может быть, есть способ обойти это, но я еще не очень старался.
  • Я еще не проводил много испытаний, поэтому могут возникнуть некоторые тонкие (или не очень) проблемы.
  • Я не думаю, что в C ++ возможно полностью бесшовное решение, которое удовлетворяет всем вашим требованиям без какого-либо изменения синтаксиса. (хотя я бы хотел оказаться неправым!) Надеюсь, это достаточно близко.
  • Когда я исследовал этот ответ, я получил большую помощь от этой очень обширной статьи по реализации делегатов функций-членов в C ++. Любой, кто хочет узнать больше, чем они думали, может узнать об указателях на функции-члены, должен прочесть эту статью.
1 голос
/ 09 августа 2009

Это выглядит как простая задача, если предположить, что «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);
   }
};
1 голос
/ 09 августа 2009

Я не думаю, что это можно легко сделать, используя только оболочку, так как вам придется перехватывать вызовы ввода-вывода, поэтому перенос класса приведет к тому, что код окажется на неправильном уровне. По сути, вы хотите заменить IO-код под объектом, но вы пытаетесь сделать это с верхнего уровня. Если вы думаете о коде как о луковице, вы пытаетесь изменить внешнюю оболочку, чтобы затронуть что-то в двух или трех слоях; ИМХО, что говорит о том, что дизайн может нуждаться в переосмыслении.

Если класс, который вы пытаетесь обернуть / изменить таким способом, действительно позволяет вам передавать поток (или любой другой механизм ввода-вывода, который вы используете), то замена этого кэширующего была бы правильной вещью; по сути, это было бы тем, чего вы пытаетесь достичь с помощью своей обертки.

0 голосов
/ 11 августа 2009

Я не разобрался в случае обработки методов объекта, но, думаю, у меня есть хорошее исправление для обычных функций

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

Какие-нибудь советы о том, как заставить это работать для объектов?

0 голосов
/ 09 августа 2009

Я думаю, что вам нужно что-то вроде прокси / декоратор (шаблоны проектирования). Вы можете использовать шаблоны, если вам не нужна динамическая часть этих шаблонов. Дело в том, что вам нужно четко определить интерфейс, который вам понадобится.

...