web-dev-qa-db-fra.com

Évaluation paresseuse en C++

C++ ne prend pas en charge nativement les évaluations paresseuses (contrairement à Haskell).

Je me demande s'il est possible de mettre en œuvre une évaluation paresseuse en C++ de manière raisonnable. Si oui, comment le ferais-tu?

EDIT: J'aime la réponse de Konrad Rudolph.

Je me demande s'il est possible de l'implémenter de manière plus générique, en utilisant par exemple une classe paresseuse paramétrée qui fonctionne essentiellement pour T comme le fait que matrix_add fonctionne pour matrix.

Toute opération sur T rendrait paresseux à la place. Le seul problème est de stocker les arguments et le code d'opération à l'intérieur de paresseux. Quelqu'un peut-il voir comment améliorer cela?

53
user51568

Je me demande s'il est possible de mettre en œuvre une évaluation paresseuse en C++ de manière raisonnable. Si oui, comment le ferais-tu?

Oui, cela est possible et assez souvent fait, par exemple pour les calculs matriciels. Le mécanisme principal pour faciliter ceci est la surcharge de l'opérateur. Considérons le cas de l'addition de matrice. La signature de la fonction ressemblerait généralement à ceci:

matrix operator +(matrix const& a, matrix const& b);

Maintenant, pour rendre cette fonction paresseuse, il suffit de renvoyer un proxy au lieu du résultat réel:

struct matrix_add;

matrix_add operator +(matrix const& a, matrix const& b) {
    return matrix_add(a, b);
}

Il ne reste plus qu’à écrire ce proxy:

struct matrix_add {
    matrix_add(matrix const& a, matrix const& b) : a(a), b(b) { }

    operator matrix() const {
        matrix result;
        // Do the addition.
        return result;
    }
private:
    matrix const& a, b;
};

La magie réside dans la méthode operator matrix() qui est un opérateur de conversion implicite de matrix_add à plain matrix. De cette façon, vous pouvez chaîner plusieurs opérations (en fournissant bien sûr les surcharges appropriées). L'évaluation n'a lieu que lorsque le résultat final est affecté à une instance matrix.

EDITJ'aurais dû être plus explicite. Dans l'état actuel des choses, le code n'a pas de sens car, bien que l'évaluation se déroule lentement, elle se produit toujours dans la même expression. En particulier, un autre ajout évaluera ce code sauf si la structure matrix_add est modifiée pour permettre l’ajout en chaîne. C++ 0x facilite grandement cela en permettant des modèles variadiques (c'est-à-dire des listes de modèles de longueur variable).

Cependant, un cas très simple où ce code aurait en réalité un avantage réel et direct est le suivant:

int value = (A + B)(2, 3);

Ici, on suppose que A et B sont des matrices bidimensionnelles et que le déréférencement est effectué en notation Fortran, c'est-à-dire que l'élément ci-dessus calcule un élément à partir d'une somme de matrice. Il est bien sûr inutile d'ajouter les matrices entières. matrix_add à la rescousse:

struct matrix_add {
    // … yadda, yadda, yadda …

    int operator ()(unsigned int x, unsigned int y) {
        // Calculate *just one* element:
        return a(x, y) + b(x, y);
    }
};

D'autres exemples abondent. Je viens de me rappeler que j'ai implémenté quelque chose de similaire il n'y a pas longtemps. Fondamentalement, je devais implémenter une classe de chaîne qui devait adhérer à une interface fixe et prédéfinie. Cependant, ma classe de chaînes particulière traitait d'énormes chaînes qui n'étaient pas réellement stockées en mémoire. Généralement, l'utilisateur accède simplement aux petites sous-chaînes de la chaîne d'origine à l'aide de la fonction infix. J'ai surchargé cette fonction pour mon type de chaîne afin de renvoyer un proxy contenant une référence à ma chaîne, ainsi que la position de début et de fin souhaitée. Ce n'est que lorsque cette sous-chaîne a réellement été utilisée qu'elle a interrogé une API C pour récupérer cette partie de la chaîne.

81
Konrad Rudolph

Boost.Lambda est très gentil, mais Boost.Proto est exactement ce que vous recherchez. Il contient déjà des surcharges d’opérateurs all C++ qui, par défaut, remplissent leur fonction habituelle lorsque proto::eval() est appelé, mais peuvent être modifiés.

29
j_random_hacker

Ce que Konrad a déjà expliqué peut être approfondi pour prendre en charge les appels imbriqués d’opérateurs, tous exécutés paresseusement. Dans l'exemple de Konrad, il a un objet d'expression pouvant stocker exactement deux arguments, pour exactement deux opérandes d'une opération. Le problème est qu’elle n’exécutera que one sous-expression paresseusement, ce qui explique joliment le concept d’évaluation paresseuse formulé en termes simples, sans pour autant améliorer sensiblement les performances. L’autre exemple montre également comment on peut appliquer operator() pour n’ajouter que quelques éléments utilisant cet objet expression. Mais pour évaluer des expressions complexes arbitraires, nous avons besoin d’un mécanisme qui puisse stocker la structure de cela aussi. Nous ne pouvons pas nous passer de modèles pour le faire. Et le nom pour cela est expression templates. L'idée est qu'un objet d'expression basé sur un modèle puisse stocker la structure d'une sous-expression arbitraire de manière récursive, comme un arbre, où les opérations sont les noeuds et les opérandes sont les noeuds enfants. Pour une très bonne explication que je viens de trouver aujourd'hui (quelques jours après avoir écrit le code ci-dessous), voir ici .

template<typename Lhs, typename Rhs>
struct AddOp {
    Lhs const& lhs;
    Rhs const& rhs;

    AddOp(Lhs const& lhs, Rhs const& rhs):lhs(lhs), rhs(rhs) {
        // empty body
    }

    Lhs const& get_lhs() const { return lhs; }
    Rhs const& get_rhs() const { return rhs; }
};

Cela stockera toute opération d'addition, même imbriquée, comme le montre la définition suivante d'un opérateur + pour un type de point simple:

struct Point { int x, y; };

// add expression template with point at the right
template<typename Lhs, typename Rhs> AddOp<AddOp<Lhs, Rhs>, Point> 
operator+(AddOp<Lhs, Rhs> const& lhs, Point const& p) {
    return AddOp<AddOp<Lhs, Rhs>, Point>(lhs, p);
} 

// add expression template with point at the left
template<typename Lhs, typename Rhs> AddOp< Point, AddOp<Lhs, Rhs> > 
operator+(Point const& p, AddOp<Lhs, Rhs> const& rhs) {
    return AddOp< Point, AddOp<Lhs, Rhs> >(p, rhs);
}

// add two points, yield a expression template    
AddOp< Point, Point > 
operator+(Point const& lhs, Point const& rhs) {
    return AddOp<Point, Point>(lhs, rhs);
}

Maintenant, si vous avez

Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 };
p1 + (p2 + p3); // returns AddOp< Point, AddOp<Point, Point> >

Vous devez maintenant surcharger l'opérateur = et ajouter un constructeur approprié pour le type Point et accepter AddOp. Changer sa définition en:

struct Point { 
    int x, y; 

    Point(int x = 0, int y = 0):x(x), y(y) { }

    template<typename Lhs, typename Rhs>
    Point(AddOp<Lhs, Rhs> const& op) {
        x = op.get_x();
        y = op.get_y();
    }

    template<typename Lhs, typename Rhs>
    Point& operator=(AddOp<Lhs, Rhs> const& op) {
        x = op.get_x();
        y = op.get_y();
        return *this;
    }

    int get_x() const { return x; }
    int get_y() const { return y; }
};

Et ajoutez les get_x et get_y appropriés à AddOp en tant que fonctions membres:

int get_x() const {
    return lhs.get_x() + rhs.get_x();
}

int get_y() const {
    return lhs.get_y() + rhs.get_y();
}

Notez que nous n’avons pas créé de temporaires de type Point. Cela aurait pu être une grande matrice avec de nombreux champs. Mais au moment où le résultat est nécessaire, nous le calculons paresseusement

25

Je n'ai rien à ajouter au message de Konrad, mais vous pouvez regarder Eigen pour un exemple d'évaluation paresseuse bien faite, dans une application du monde réel. C'est assez impressionnant.

9
Iraimbilanja

La réponse de Johannes fonctionne. Mais quand il s'agit de plusieurs parenthèses, cela ne fonctionne pas comme souhaité. Voici un exemple.

Point p1 = { 1, 2 }, p2 = { 3, 4 }, p3 = { 5, 6 }, p4 = { 7, 8 };
(p1 + p2) + (p3+p4)// it works ,but not lazy enough

Parce que les trois opérateurs surchargés + n'ont pas couvert l'affaire 

AddOp<Llhs,Lrhs>+AddOp<Rlhs,Rrhs>

Ainsi, le compilateur doit convertir (p1 + p2) ou (p3 + p4) en point, ce n’est pas assez paresseux. Et quand le compilateur décide lequel convertir, il se plaint. Parce qu’aucun n’est meilleur que les autres . Voici mon extension: ajouter encore un autre opérateur surchargé +

    template <typename LLhs, typename LRhs, typename RLhs, typename RRhs>
AddOp<AddOp<LLhs, LRhs>, AddOp<RLhs, RRhs>> operator+(const AddOp<LLhs, LRhs> & leftOperandconst, const AddOp<RLhs, RRhs> & rightOperand)
{
    return  AddOp<AddOp<LLhs, LRhs>, AddOp<RLhs, RRhs>>(leftOperandconst, rightOperand);

}

Maintenant, le compilateur peut gérer le cas ci-dessus correctement, et aucune conversion implicite, volia!

3
spiritsaway

Je songe à implémenter une classe de modèle utilisant std::function. La classe devrait, plus ou moins, ressembler à ceci:

template <typename Value>
class Lazy
{
public:
    Lazy(std::function<Value()> function) : _function(function), _evaluated(false) {}

    Value &operator*()  { Evaluate(); return  _value; }
    Value *operator->() { Evaluate(); return &_value; }

private:
    void Evaluate()
    {
        if (!_evaluated)
        {
            _value = _function();
            _evaluated = true;
        }
    }

    std::function<Value()> _function;
    Value _value;
    bool _evaluated;
};

Par exemple utilisation:

class Noisy
{
public:
    Noisy(int i = 0) : _i(i)
    {
        std::cout << "Noisy(" << _i << ")"  << std::endl;
    }
    Noisy(const Noisy &that) : _i(that._i)
    {
        std::cout << "Noisy(const Noisy &)" << std::endl;
    }
    ~Noisy()
    {
        std::cout << "~Noisy(" << _i << ")" << std::endl;
    }

    void MakeNoise()
    {
        std::cout << "MakeNoise(" << _i << ")" << std::endl;
    }
private:
    int _i;
};  

int main()
{
    Lazy<Noisy> n = [] () { return Noisy(10); };

    std::cout << "about to make noise" << std::endl;

    n->MakeNoise();
    (*n).MakeNoise();
    auto &nn = *n;
    nn.MakeNoise();
}

Le code ci-dessus devrait produire le message suivant sur la console:

Noisy(0)
about to make noise
Noisy(10)
~Noisy(10)
MakeNoise(10)
MakeNoise(10)
MakeNoise(10)
~Noisy(10)

Notez que le constructeur qui imprime Noisy(10) ne sera appelé que lorsque la variable aura été utilisée.

Cette classe est loin d'être parfaite, cependant. La première chose à faire serait que le constructeur par défaut de Value devra être appelé à l'initialisation du membre (impression Noisy(0) dans ce cas). Nous pouvons utiliser le pointeur pour _value à la place, mais je ne suis pas sûr que cela affecterait les performances.

3
hiapay

Tout est possible.

Cela dépend de ce que vous voulez dire:

class X
{
     public: static X& getObjectA()
     {
          static X instanceA;

          return instanceA;
     }
};

Nous avons ici l’effet d’une variable globale qui est évaluée paresseusement au moment de la première utilisation.

Comme récemment demandé dans la question.
Et voler Konrad Rudolph et le prolonger.

L'objet paresseux:

template<typename O,typename T1,typename T2>
struct Lazy
{
    Lazy(T1 const& l,T2 const& r)
        :lhs(l),rhs(r) {}

    typedef typename O::Result  Result;
    operator Result() const
    {
        O   op;
        return op(lhs,rhs);
    }
    private:
        T1 const&   lhs;
        T2 const&   rhs;
};

Comment l'utiliser:

namespace M
{
    class Matrix
    {
    };
    struct MatrixAdd
    {
        typedef Matrix  Result;
        Result operator()(Matrix const& lhs,Matrix const& rhs) const
        {
            Result  r;
            return r;
        }
    };
    struct MatrixSub
    {
        typedef Matrix  Result;
        Result operator()(Matrix const& lhs,Matrix const& rhs) const
        {
            Result  r;
            return r;
        }
    };
    template<typename T1,typename T2>
    Lazy<MatrixAdd,T1,T2> operator+(T1 const& lhs,T2 const& rhs)
    {
        return Lazy<MatrixAdd,T1,T2>(lhs,rhs);
    }
    template<typename T1,typename T2>
    Lazy<MatrixSub,T1,T2> operator-(T1 const& lhs,T2 const& rhs)
    {
        return Lazy<MatrixSub,T1,T2>(lhs,rhs);
    }
}
2
Martin York

C++ 0x est agréable et tout .... mais pour ceux d'entre nous qui vivent dans le présent, vous avez la bibliothèque Boost lambda et Boost Phoenix. Les deux dans le but d’apporter de grandes quantités de programmation fonctionnelle au C++.

2
Hippiehunter

Comme cela va être fait dans C++ 0x , par les expressions lambda.

1
Mehrdad Afshari

En C++ 11, une évaluation lazy similaire à celle de hiapay peut être obtenue avec std :: shared_future. Vous devez encore encapsuler les calculs dans lambdas mais la mémoisation est prise en charge:

std::shared_future<int> a = std::async(std::launch::deferred, [](){ return 1+1; });

Voici un exemple complet:

#include <iostream>
#include <future>

#define LAZY(EXPR, ...) std::async(std::launch::deferred, [__VA_ARGS__](){ std::cout << "evaluating "#EXPR << std::endl; return EXPR; })

int main() {
    std::shared_future<int> f1 = LAZY(8);
    std::shared_future<int> f2 = LAZY(2);
    std::shared_future<int> f3 = LAZY(f1.get() * f2.get(), f1, f2);

    std::cout << "f3 = " << f3.get() << std::endl;
    std::cout << "f2 = " << f2.get() << std::endl;
    std::cout << "f1 = " << f1.get() << std::endl;
    return 0;
}
1
user2259659

En utilisant une définition très simple de l'évaluation paresseuse, à savoir que la valeur n'est pas évaluée avant d'être utilisée, je dirais qu'on pourrait l'implémenter en utilisant un pointeur et des macros (pour le sucre de syntaxe).

#include <stdatomic.h>

#define lazy(var_type) lazy_ ## var_type

#define def_lazy_type( var_type ) \
    typedef _Atomic var_type _atomic_ ## var_type; \
    typedef _atomic_ ## var_type * lazy(var_type);  //pointer to atomic type

#define def_lazy_variable(var_type, var_name ) \
    _atomic_ ## var_type _ ## var_name; \
    lazy_ ## var_type var_name = & _ ## var_name;

#define assign_lazy( var_name, val ) atomic_store( & _ ## var_name, val )
#define eval_lazy(var_name) atomic_load( &(*var_name) )

#include <stdio.h>

def_lazy_type(int)

void print_power2 ( lazy(int) i )
{
      printf( "%d\n", eval_lazy(i) * eval_lazy(i) );
}

typedef struct {
    int a;
} simple;

def_lazy_type(simple)

void print_simple ( lazy(simple) s )
{
    simple temp = eval_lazy(s);
    printf("%d\n", temp.a );
}


#define def_lazy_array1( var_type, nElements, var_name ) \
    _atomic_ ## var_type  _ ## var_name [ nElements ]; \
    lazy(var_type) var_name = _ ## var_name; 

int main ( )
{
    //declarations
    def_lazy_variable( int, X )
    def_lazy_variable( simple, Y)
    def_lazy_array1(int,10,Z)
    simple new_simple;

    //first the lazy int
    assign_lazy(X,111);
    print_power2(X);

    //second the lazy struct
    new_simple.a = 555;
    assign_lazy(Y,new_simple);
    print_simple ( Y );

    //third the array of lazy ints
    for(int i=0; i < 10; i++)
    {
        assign_lazy( Z[i], i );
    }

    for(int i=0; i < 10; i++)
    {
        int r = eval_lazy( &Z[i] ); //must pass with &
        printf("%d\n", r );
    }

    return 0;
}

Vous remarquerez dans la fonction print_power2 qu'il existe une macro appelée eval_lazy qui ne fait que déréférencer un pointeur pour obtenir la valeur juste avant son utilisation. Le type paresseux est accessible de manière atomique, il est donc totalement thread-safe.

0
annoying_squid