Question

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

Je me demande s'il est possible d'implémenter l'évaluation lazy en C ++ de manière raisonnable. Si oui, comment le feriez-vous?

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, par exemple en utilisant une classe paramétrée paresseuse qui fonctionne essentiellement pour T comme le fait que matrix_add fonctionne pour matrix.

Toute opération sur T renverrait plutôt paresseux. 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?

Était-ce utile?

La solution

  

Je me demande s'il est possible d'implémenter l'évaluation lazy en C ++ de manière raisonnable. Si oui, comment le feriez-vous?

Oui, cela est possible et assez souvent, par exemple. pour les calculs matriciels. Le mécanisme principal pour faciliter cela 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 A instance.

MODIFIER J'aurais dû être plus explicite. Dans l'état actuel des choses, le code n'a pas de sens car, bien que l'évaluation se produise paresseusement, elle se produit toujours dans la même expression. En particulier, un autre ajout évaluera ce code à moins que la structure B ne soit 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 infix et <=> 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 sur une somme de matrice. Il est bien sûr inutile d'ajouter les matrices entières. <=> à 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. Habituellement, l'utilisateur accède simplement aux petites sous-chaînes de la chaîne d'origine à l'aide d'une fonction <=>. 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.

Autres conseils

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

Ce que Konrad a déjà expliqué peut être ajouté 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 une sous-expression paresseusement, ce qui explique bien le concept de l’évaluation paresseuse en termes simples, mais n’améliore pas considérablement 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 celui-ci également. 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 nœuds et les opérandes sont les nœuds 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. Changez 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 dans 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 .

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

Je songe à implémenter une classe de modèle utilisant std::function. La classe devrait ressembler plus ou moins à 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 doit générer 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 l'impression du constructeur Noisy(10) ne sera pas appelée avant l'accès à la variable.

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.

La réponse de Johannes fonctionne. Mais quand il s'agit de plus de 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 + ne couvraient pas le cas

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

Donc, le compilateur doit convertir soit (p1 + p2), soit (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 l'autre. 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, sans conversion implicite, volia!

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

Tout est possible.

Cela dépend de ce que vous voulez dire exactement:

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 la conception de Konrad Rudolph et l’étendre.

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

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

Dans C ++ 11, une évaluation lazy similaire à celle de hiapay peut être obtenue à l'aide de 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;
}

Lets take Haskell as our inspiration - it being lazy to the core. Also, let's keep in mind how Linq in C# uses Enumerators in a monadic (urgh - here is the word - sorry) way. Last not least, lets keep in mind, what coroutines are supposed to provide to programmers. Namely the decoupling of computational steps (e.g. producer consumer) from each other. And lets try to think about how coroutines relate to lazy evaluation.

All of the above appears to be somehow related.

Next, lets try to extract our personal definition of what "lazy" comes down to.

One interpretation is: We want to state our computation in a composable way, before executing it. Some of those parts we use to compose our complete solution might very well draw upon huge (sometimes infinite) data sources, with our full computation also either producing a finite or infinite result.

Lets get concrete and into some code. We need an example for that! Here, I choose the fizzbuzz "problem" as an example, just for the reason that there is some nice, lazy solution to it.

In Haskell, it looks like this:

module FizzBuzz
( fb
)
where
fb n =
    fmap merge fizzBuzzAndNumbers
    where
        fizz = cycle ["","","fizz"]
        buzz = cycle ["","","","","buzz"]
        fizzBuzz = zipWith (++) fizz buzz
        fizzBuzzAndNumbers = zip [1..n] fizzBuzz
        merge (x,s) = if length s == 0 then show x else s

The Haskell function cycle creates an infinite list (lazy, of course!) from a finite list by simply repeating the values in the finite list forever. In an eager programming style, writing something like that would ring alarm bells (memory overflow, endless loops!). But not so in a lazy language. The trick is, that lazy lists are not computed right away. Maybe never. Normally only as much as subsequent code requires it.

The third line in the where block above creates another lazy!! list, by means of combining the infinite lists fizz and buzz by means of the single two elements recipe "concatenate a string element from either input list into a single string". Again, if this were to be immediately evaluated, we would have to wait for our computer to run out of resources.

In the 4th line, we create tuples of the members of a finite lazy list [1..n] with our infinite lazy list fizzbuzz. The result is still lazy.

Even in the main body of our fb function, there is no need to get eager. The whole function returns a list with the solution, which itself is -again- lazy. You could as well think of the result of fb 50 as a computation which you can (partially) evaluate later. Or combine with other stuff, leading to an even larger (lazy) evaluation.

So, in order to get started with our C++ version of "fizzbuzz", we need to think of ways how to combine partial steps of our computation into larger bits of computations, each drawing data from previous steps as required.

You can see the full story in a gist of mine.

Here the basic ideas behind the code:

Borrowing from C# and Linq, we "invent" a stateful, generic type Enumerator, which holds
- The current value of the partial computation
- The state of a partial computation (so we can produce subsequent values)
- The worker function, which produces the next state, the next value and a bool which states if there is more data or if the enumeration has come to an end.

In order to be able to compose Enumerator<T,S> instance by means of the power of the . (dot), this class also contains functions, borrowed from Haskell type classes such as Functor and Applicative.

The worker function for enumerator is always of the form: S -> std::tuple<bool,S,T where S is the generic type variable representing the state and T is the generic type variable representing a value - the result of a computation step.

All this is already visible in the first lines of the Enumerator class definition.

template <class T, class S>
class Enumerator
{
public:
    typedef typename S State_t;
    typedef typename T Value_t;
    typedef std::function<
        std::tuple<bool, State_t, Value_t>
        (const State_t&
            )
    > Worker_t;

    Enumerator(Worker_t worker, State_t s0)
        : m_worker(worker)
        , m_state(s0)
        , m_value{}
    {
    }
    // ...
};

So, all we need to create a specific enumerator instance, we need to create a worker function, have the initial state and create an instance of Enumerator with those two arguments.

Here an example - function range(first,last) creates a finite range of values. This corresponds to a lazy list in the Haskell world.

template <class T>
Enumerator<T, T> range(const T& first, const T& last)
{
    auto finiteRange =
        [first, last](const T& state)
    {
        T v = state;
        T s1 = (state < last) ? (state + 1) : state;
        bool active = state != s1;
        return std::make_tuple(active, s1, v);
    };
    return Enumerator<T,T>(finiteRange, first);
}

And we can make use of this function, for example like this: auto r1 = range(size_t{1},10); - We have created ourselves a lazy list with 10 elements!

Now, all is missing for our "wow" experience, is to see how we can compose enumerators. Coming back to Haskells cycle function, which is kind of cool. How would it look in our C++ world? Here it is:

template <class T, class S>
auto
cycle
( Enumerator<T, S> values
) -> Enumerator<T, S>
{
    auto eternally =
        [values](const S& state) -> std::tuple<bool, S, T>
    {
        auto[active, s1, v] = values.step(state);
        if (active)
        {
            return std::make_tuple(active, s1, v);
        }
        else
        {
            return std::make_tuple(true, values.state(), v);
        }
    };
    return Enumerator<T, S>(eternally, values.state());
}

It takes an enumerator as input and returns an enumerator. Local (lambda) function eternally simply resets the input enumeration to its start value whenever it runs out of values and voilà - we have an infinite, ever repeating version of the list we gave as an argument:: auto foo = cycle(range(size_t{1},3)); And we can already shamelessly compose our lazy "computations".

zip is a good example, showing that we can also create a new enumerator from two input enumerators. The resulting enumerator yields as many values as the smaller of either of the input enumerators (tuples with 2 element, one for each input enumerator). I have implemented zip inside class Enumerator itself. Here is how it looks like:

// member function of class Enumerator<S,T> 
template <class T1, class S1>
auto
zip
( Enumerator<T1, S1> other
) -> Enumerator<std::tuple<T, T1>, std::tuple<S, S1> >
{
    auto worker0 = this->m_worker;
    auto worker1 = other.worker();
    auto combine =
        [worker0,worker1](std::tuple<S, S1> state) ->
        std::tuple<bool, std::tuple<S, S1>, std::tuple<T, T1> >
    {
        auto[s0, s1] = state;
        auto[active0, newS0, v0] = worker0(s0);
        auto[active1, newS1, v1] = worker1(s1);
        return std::make_tuple
            ( active0 && active1
            , std::make_tuple(newS0, newS1)
            , std::make_tuple(v0, v1)
            );
    };
    return Enumerator<std::tuple<T, T1>, std::tuple<S, S1> >
        ( combine
        , std::make_tuple(m_state, other.state())
        );
}

Please note, how the "combining" also ends up in combining the state of both sources and the values of both sources.

As this post is already TL;DR; for many, here the...

Summary

Yes, lazy evaluation can be implemented in C++. Here, I did it by borrowing the function names from haskell and the paradigm from C# enumerators and Linq. There might be similarities to pythons itertools, btw. I think they followed a similar approach.

My implementation (see the gist link above) is just a prototype - not production code, btw. So no warranties whatsoever from my side. It serves well as demo code to get the general idea across, though.

And what would this answer be without the final C++ version of fizzbuz, eh? Here it is:

std::string fizzbuzz(size_t n)
{
    typedef std::vector<std::string> SVec;
    // merge (x,s) = if length s == 0 then show x else s
    auto merge =
        [](const std::tuple<size_t, std::string> & value)
        -> std::string
    {
        auto[x, s] = value;
        if (s.length() > 0) return s; 
        else return std::to_string(x);
    };

    SVec fizzes{ "","","fizz" };
    SVec buzzes{ "","","","","buzz" };

    return
    range(size_t{ 1 }, n)
    .zip
        ( cycle(iterRange(fizzes.cbegin(), fizzes.cend()))
          .zipWith
            ( std::function(concatStrings)
            , cycle(iterRange(buzzes.cbegin(), buzzes.cend()))
            )
        )
    .map<std::string>(merge)
    .statefulFold<std::ostringstream&>
    (
        [](std::ostringstream& oss, const std::string& s) 
        {
            if (0 == oss.tellp())
            {
                oss << s;
            }
            else
            {
                oss << "," << s;
            }
        }
        , std::ostringstream()
    )
    .str();
}

And... to drive the point home even further - here a variation of fizzbuzz which returns an "infinite list" to the caller:

typedef std::vector<std::string> SVec;
static const SVec fizzes{ "","","fizz" };
static const SVec buzzes{ "","","","","buzz" };

auto fizzbuzzInfinite() -> decltype(auto)
{
    // merge (x,s) = if length s == 0 then show x else s
    auto merge =
        [](const std::tuple<size_t, std::string> & value)
        -> std::string
    {
        auto[x, s] = value;
        if (s.length() > 0) return s;
        else return std::to_string(x);
    };

    auto result =
        range(size_t{ 1 })
        .zip
        (cycle(iterRange(fizzes.cbegin(), fizzes.cend()))
            .zipWith
            (std::function(concatStrings)
                , cycle(iterRange(buzzes.cbegin(), buzzes.cend()))
            )
        )
        .map<std::string>(merge)
        ;
    return result;
}

It is worth showing, since you can learn from it how to dodge the question what the exact return type of that function is (as it depends on the implementation of the function alone, namely how the code combines the enumerators).

Also it demonstrates that we had to move the vectors fizzes and buzzes outside the scope of the function so they are still around when eventually on the outside, the lazy mechanism produces values. If we had not done that, the iterRange(..) code would have stored iterators to the vectors which are long gone.

Using a very simple definition of lazy evaluation, which is the value is not evaluated until needed, I would say that one could implement this through the use of a pointer and macros (for syntax sugar).

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

You'll notice in the function print_power2 there is a macro called eval_lazy which does nothing more than dereference a pointer to get the value just prior to when it's actually needed. The lazy type is accessed atomically, so it's completely thread-safe.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top