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

Итак, предположим, что у нас есть 5 функций в нашей программе f1...f5, и мы хотим избежать (дорогостоящего) повторного вычисления для функций f1 и f3, если мы уже вызывали их с тем же ввод. Обратите внимание, что каждая функция может иметь разные типы возвращаемых значений и аргументов .

Я нашел это решение проблемы, но вы можете использовать только double и { {X1}}.

МОЕ РЕШЕНИЕ

Хорошо, я написал это решение для своей проблемы, но я не знаю, эффективно ли оно, безопасно или может быть написано более элегантным способом.

template <typename ReturnType, typename... Args>
function<ReturnType(Args...)> memoize(function<ReturnType(Args...)> func)
{
    return ([=](Args... args) mutable {
        static map<tuple<Args...>, ReturnType> cache;
        tuple<Args...> t(args...);
        auto result = cache.insert(make_pair(t, ReturnType{}));
        if (result.second) {
            // insertion succeeded so the value wasn't cached already
            result.first->second = func(args...);
        }
        return result.first->second;
    });
}

struct MultiMemoizator
{
    map<string, boost::any> multiCache;
    template <typename ReturnType, typename... Args>
    void addFunction(string name, function < ReturnType(Args...)> func) {
        function < ReturnType(Args...)> cachedFunc = memoize(func);
        boost::any anyCachedFunc = cachedFunc;
        auto result = multiCache.insert(pair<string, boost::any>(name,anyCachedFunc));
        if (!result.second)
            cout << "ERROR: key " + name + " was already inserted" << endl;
    }
    template <typename ReturnType, typename... Args>
    ReturnType callFunction(string name, Args... args) {
        auto it = multiCache.find(name);
        if (it == multiCache.end())
            throw KeyNotFound(name);
        boost::any anyCachedFunc = it->second;
        function < ReturnType(Args...)> cachedFunc = boost::any_cast<function<ReturnType(Args...)>> (anyCachedFunc);
        return cachedFunc(args...);
    }
};

И это возможный основной:

int main()
{
    function<int(int)> intFun = [](int i) {return ++i; };
    function<string(string)> stringFun = [](string s) {
        return "Hello "+s;
    };
    MultiMemoizator mem;
    mem.addFunction("intFun",intFun);
    mem.addFunction("stringFun", stringFun);
    try
    {
        cout << mem.callFunction<int, int>("intFun", 1)<<endl;//print 2
        cout << mem.callFunction<string, string>("stringFun", " World!") << endl;//print Hello World!
        cout << mem.callFunction<string, string>("TrumpIsADickHead", " World!") << endl;//KeyNotFound thrown
    }
    catch (boost::bad_any_cast e)
    {
        cout << "Bad function calling: "<<e.what()<<endl;
        return 1;
    }
    catch (KeyNotFound e) 
    {
        cout << e.what()<<endl;
        return 1;
    }
}
3
justHelloWorld 21 Апр 2016 в 06:32

5 ответов

Лучший ответ

Как насчет чего-то вроде этого:

template <typename result_t, typename... args_t>
class Memoizer
{
public:
    typedef result_t (*function_t)(args_t...);
    Memoizer(function_t func) : m_func(func) {}

    result_t operator() (args_t... args)
    {
        auto args_tuple = make_tuple(args...);

        auto it = m_results.find(args_tuple);
        if (it != m_results.end())
            return it->second;

        result_t result = m_func(args...);
        m_results.insert(make_pair(args_tuple, result));
        return result;
    }

protected:
    function_t m_func;
    map<tuple<args_t...>, result_t> m_results;
};

Использование такое:

// could create make_memoizer like make_tuple to eliminate the template arguments
Memoizer<double, double> memo(fabs);
cout << memo(-123.456);
cout << memo(-123.456); // not recomputed
1
John Zwinck 21 Апр 2016 в 04:03

Довольно сложно догадаться, как вы планируете использовать функции, с мемоизацией или без нее, но для аспекта контейнера различных - function<> вам просто нужен общий базовый класс:

#include <iostream>
#include <vector>
#include <functional>

struct Any_Function
{
    virtual ~Any_Function() {}
};

template <typename Ret, typename... Args>
struct Function : Any_Function, std::function<Ret(Args...)>
{
    template <typename T>
    Function(T& f)
      : std::function<Ret(Args...)>(f)
    { }
};

int main()
{
    std::vector<Any_Function*> fun_vect;
    auto* p = new Function<int, double, double, int> { [](double i, double j, int z) {
        return int(i + j + z);
    } };
    fun_vect.push_back(p);
}
1
Tony Delroy 21 Апр 2016 в 04:31

Проблема в том, как сделать его типобезопасным. Взгляните на этот код:

MultiMemoizator mm;
std::string name = "identity";
mm.addFunction(name, identity);
auto result = mm.callFunction(name, 1);

Последняя строка верна? Имеет ли callFunction нужное количество параметров с нужными типами? А какой тип возврата?

Компилятор не имеет возможности узнать об этом: у него нет способа понять, что name есть "identity", и даже если это так, нет способа связать это с типом функции. И это не относится к C ++, любой статически типизированный язык будет иметь такую же проблему.

Одно из решений (в основном то, что дано в ответе Тони Д.) - сообщить компилятору сигнатуру функции при ее вызове. А если вы скажете это неправильно, произойдет ошибка времени выполнения. Это может выглядеть примерно так (вам нужно только явно указать тип возвращаемого значения, поскольку количество и тип параметров являются предполагаемыми):

auto result = mm.callFunction<int>(name, 1);

Но это неэлегантно и чревато ошибками.

В зависимости от ваших конкретных требований, что может работать лучше, так это использовать «умные» ключи вместо строк: ключ имеет сигнатуру функции, встроенную в его тип, поэтому вам не нужно беспокоиться о ее правильном указании. Это может выглядеть примерно так:

Key<int(int)> identityKey;
mm.addFunction(identityKey, identity);
auto result = mm.callFunction(identityKey, 1);

Таким образом, типы проверяются во время компиляции (как для addFunction, так и для callFunction), что должно дать вам именно то, что вы хотите.

На самом деле я не реализовал это на C ++, но я не вижу причин, почему это должно быть сложно или невозможно. Тем более, что сделать что-то очень похожее на C # очень просто.

1
Community 13 Апр 2017 в 12:40

Вы можете использовать вектор функций с сигнатурой, например void someFunction(void *r, ...), где r - указатель на результат, а ... - список переменных с переменным числом аргументов. Предупреждение: распаковка списка аргументов действительно неудобна и больше похожа на взлом.

0
Andrei R. 21 Апр 2016 в 04:00

На первый взгляд, как насчет определения типа с аргументами шаблона, которые различаются для каждой функции, то есть:

template <class RetType, class ArgType> 
class AbstractFunction {
    //etc.
}

Пусть AbstractFunction принимает указатель на функции f1-f5 со специализацией шаблона, разной для каждой функции. Затем у вас может быть общая функция run_memoized () либо как функция-член AbstractFunction, либо как шаблонная функция, которая принимает AbstractFunction в качестве аргумента и поддерживает памятку по мере ее выполнения.

Самая сложная часть будет заключаться в том, что функции f1-f5 имеют более одного аргумента, и в этом случае вам нужно будет сделать некоторые забавные вещи с arglists в качестве параметров шаблона, но я думаю, что C ++ 14 имеет некоторые функции, которые могут сделать это возможным. Альтернативой является переписывание f1-f5 так, чтобы все они принимали одну структуру в качестве аргумента, а не несколько аргументов.

EDIT: увидев вашу проблему 1, проблема, с которой вы столкнулись, заключается в том, что вы хотите иметь структуру данных, значения которой являются мемоизированными функциями, каждая из которых может иметь разные аргументы.

Я лично решил бы эту проблему, просто заставив структуру данных использовать void * для представления отдельных мемоизированных функций, а затем в методе callFunction () использовать небезопасное приведение типа от void * к шаблонному типу MemoizedFunction, который вам нужен (вам может потребоваться выделить MemoizedFunctions оператором new, чтобы вы могли преобразовать их в void * s и обратно.)

Если отсутствие безопасности типов здесь вас раздражает, хорошо для вас, в этом случае может быть разумным вариантом просто создать рукописные вспомогательные методы для каждого из f1-f5 и заставить callFunction () отправлять одну из этих функций на основе строка ввода. Это позволит вам использовать проверку типов во время компиляции.

РЕДАКТИРОВАТЬ №2: Если вы собираетесь использовать этот подход, вам нужно немного изменить API для callFunction (), чтобы callFunction имел аргументы шаблона, соответствующие типам возврата и аргумента функции, например:

int result = callFunction<int, arglist(double, float)>("double_and_float_to_int", 3.5, 4);

И если пользователь этого API когда-либо неправильно набирает тип аргумента или возвращаемые типы при использовании callFunction ... молитесь за их душу, потому что все взорвется очень уродливым образом.

РЕДАКТИРОВАТЬ # 3: вы можете до некоторой степени выполнить необходимую вам проверку типа во время выполнения, используя std :: type_info и сохранив typeid () типа аргумента и возвращаемого типа в вашей MemoizedFunction, чтобы вы могли проверить, соответствуют ли аргументы шаблона в callFunction ( ) верны перед вызовом - так вы можете предотвратить взрыв выше. Но это добавит немного накладных расходов каждый раз, когда вы вызываете функцию (вы можете обернуть это в макрос IF_DEBUG_MODE, чтобы добавить эти накладные расходы только во время тестирования, а не в производственной среде).

0
Ben Braun 21 Апр 2016 в 05:10