web-dev-qa-db-fra.com

Ecriture de la fonction de mémorisation universelle en C ++ 11

Vous cherchez un moyen de mettre en œuvre une fonction de mémorisation générique universelle qui prendra une fonction et retournera la version mémorisée de la même?

Vous cherchez quelque chose comme @memo (du site de Norving) décorateur en python.

def memo(f):
    table = {}
    def fmemo(*args):
        if args not in table:
            table[args] = f(*args)
        return table[args]
    fmemo.memo = table
    return fmemo

De manière plus générale, existe-t-il un moyen d'exprimer des décorateurs génériques et réutilisables en C++, éventuellement en utilisant les nouvelles fonctionnalités de C++ 11?

52
akrohit

Un compact retournant un lambda:

template <typename R, typename... Args>
std::function<R (Args...)> memo(R (*fn)(Args...)) {
    std::map<std::Tuple<Args...>, R> table;
    return [fn, table](Args... args) mutable -> R {
        auto argt = std::make_Tuple(args...);
        auto memoized = table.find(argt);
        if(memoized == table.end()) {
            auto result = fn(args...);
            table[argt] = result;
            return result;
        } else {
            return memoized->second;
        }
    };
}

En C++ 14, on peut utiliser la déduction de type de retour généralisé pour éviter l'indirection supplémentaire imposée en retournant std::function.

Rendre ceci entièrement général, permettant de passer des objets de fonction arbitraires sans les encapsuler dans std::function le premier est laissé comme exercice au lecteur.

40
JohannesD

La bonne façon de faire de la mémorisation en C++ est de mélanger le combinateur Y dans.

Votre fonction de base a besoin d'une modification. Au lieu de s'appeler directement, il prend une référence modèle à lui-même comme premier argument (ou une récursion std::function<Same_Signature> Comme premier argument).

Nous commençons par un combinateur Y . Ensuite, nous ajoutons un cache sur la operator() et la renommons en memoizer, et lui donnons une signature fixe (pour la table).

Il ne reste plus qu'à écrire un Tuple_hash<template<class...>class Hash> Qui fait un hachage sur un Tuple.

Le type de la fonction qui peut être mémorisée est (((Args...)->R), Args...) -> R, Ce qui rend le mémoizer de type ( (((Args...) -> R), Args...) -> R ) -> ((Args...) -> R). Avoir un combinateur Y autour pour produire une implémentation récursive "traditionnelle" peut également être utile.

Notez que si la fonction mémorisée modifie ses arguments pendant un appel, le mémorisateur mettra en cache les résultats au mauvais endroit.

struct wrap {};

template<class Sig, class F, template<class...>class Hash=std::hash>
struct memoizer;
template<class R, class...Args, class F, template<class...>class Hash>
struct memoizer<R(Args...), F, Hash> {
  using base_type = F;
private:
  F base;
  std::unordered_map< std::Tuple<std::decay_t<Args>...>, R, Tuple_hash<Hash> > cache;
public:

  template<class... Ts>
  R operator()(Ts&&... ts) const
  {
    auto args = std::make_Tuple(ts...);
    auto it = cache.find( args );
    if (it != cache.end())
      return it->second;

    auto&& retval = base(*this, std::forward<Ts>(ts)...);

    cache.emplace( std::move(args), retval );

    return decltype(retval)(retval);
  }
  template<class... Ts>
  R operator()(Ts&&... ts)
  {
    auto args = std::tie(ts...);
    auto it = cache.find( args );
    if (it != cache.end())
      return it->second;

    auto&& retval = base(*this, std::forward<Ts>(ts)...);

    cache.emplace( std::move(args), retval );

    return decltype(retval)(retval);
  }

  memoizer(memoizer const&)=default;
  memoizer(memoizer&&)=default;
  memoizer& operator=(memoizer const&)=default;
  memoizer& operator=(memoizer&&)=default;
  memoizer() = delete;
  template<typename L>
  memoizer( wrap, L&& f ):
    base( std::forward<L>(f) )
  {}
};

template<class Sig, class F>
memoizer<Sig, std::decay_t<F>> memoize( F&& f ) { return {wrap{}, std::forward<F>(f)}; }

exemple en direct avec une fonction de hachage codée en dur basée sur this SO post .

auto fib = memoize<size_t(size_t)>(
  [](auto&& fib, size_t i)->size_t{
    if (i<=1) return 1;
    return fib(i-1)+fib(i-2);
  }
);
21

Bien que @KerrekSB ait publié un lien vers une autre réponse, je pensais que je jetterais également ma réponse dans le ring (c'est probablement un peu moins compliqué que la réponse liée, bien qu'en substance elle soit très similaire):

#include <functional>
#include <map>
#include <Tuple>
#include <utility>

/*! \brief A template functor class that can be utilized to memoize any 
*          given function taking any number of arguments. 
*/
template <typename R, typename... Args>
struct memoize_wrapper
{
private:

    std::map<std::Tuple<Args...>, R> memo_;
    std::function<R(Args...)> func_;

public:

    /*! \brief Auto memoization constructor.
     *  
     *  \param func an the std::function to be memoized.
    */
    memoize_wrapper(std::function<R(Args...)> func)
      : func_(func)
    { }

    /*! \brief Memoization functor implementation.
     *  
     *  \param a Argument values that match the argument types for the 
     *           (previously) supplied function. 
     *  \return A value of return type R equivalent to calling func(a...).
     *          If this function has been called with these parameters
     *          previously, this will take O(log n) time.
    */
    R operator()(Args&&... a)
    {
        auto tup = std::make_Tuple(std::forward<Args>(a)...);
        auto it = memo_.find(tup);
        if(it != memo_.end()) {
            return it->second;
        }
        R val = func_(a...);
        memo_.insert(std::make_pair(std::move(tup), val));
        return val;
    }

}; //end struct memoize_wrapper

Edit: Exemple d'utilisation:

Edit2: Comme indiqué, cela ne fonctionne pas avec les fonctions récursives.

#include "utility/memoize_wrapper.hpp"
#include <memory>
#include <vector>
#include <algorithm>
#include <iostream>

long factorial(long i)
{
    long result = 1;
    long current = 2;
    while(current <= i) {
        result *= current;
        ++current;
    }
    return result;
}

int main()
{
    std::vector<int> arg {10, 9, 8, 7, 6, 10, 9, 8, 7, 6};
    std::transform(arg.begin(), arg.end(), arg.begin(), memoize_wrapper<long, long>(factorial));
    for(long i : arg) {
        std::cout << i << "\n";
    }
}
5
Yuushi

J'ai lutté avec le même problème. J'ai créé une macro qui prend également en charge (avec une petite modification dans le code récursif) la récursivité. C'est ici:

#include <map>
#include <Tuple>
#define MEMOIZATOR(N, R, ...)                               \
R _ ## N (__VA_ARGS__);                                     \
std::map<std::Tuple<__VA_ARGS__>, R> _memo_ ## N;           \
template <typename ... Args>                                \
R N (Args ... args) {                                       \
    auto& _memo = _memo_ ## N;                              \
    auto result = _memo.find(std::make_Tuple(args...));     \
    if (result != _memo.end()) {                            \
        return result->second;                              \
    }                                                       \
    else {                                                  \
        auto result = _ ## N  (args...);                    \
        _memo[std::make_Tuple(args...)] = result;           \
        return result;                                      \
    }                                                       \
}                                                           

L'utilisation est vraiment simple:

MEMOIZATOR(fibonacci, long int, int);

long int _fibonacci(int n) { // note the leading underscore 
                             // this makes recursive function to go through wrapper
    if (n == 1 or n == 2) {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

fibonacci(40) // uses memoizator so it works in linear time 
              // (try it with and without memoizator)

Voyez-le en action: http://ideone.com/C3JEUT :)

3
jendas