Domanda

C ++ non ha il supporto nativo per la valutazione pigra (come fa Haskell).

Mi chiedo se sia possibile implementare la valutazione pigra in C ++ in modo ragionevole. Se sì, come lo faresti?

EDIT: mi piace la risposta di Konrad Rudolph.

Mi chiedo se sia possibile implementarlo in modo più generico, ad esempio utilizzando una classe parametrica pigra che funziona essenzialmente per T come matrix_add funziona per matrice.

Qualsiasi operazione su T sarebbe invece pigra. L'unico problema è archiviare gli argomenti e il codice operazione all'interno di pigro stesso. Qualcuno può vedere come migliorare questo?

È stato utile?

Soluzione

  

Mi chiedo se sia possibile implementare la valutazione pigra in C ++ in modo ragionevole. Se sì, come lo faresti?

Sì, questo è possibile e abbastanza spesso fatto, ad es. per calcoli con matrici. Il meccanismo principale per facilitare ciò è il sovraccarico dell'operatore. Considera il caso dell'aggiunta della matrice. La firma della funzione dovrebbe apparire in questo modo:

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

Ora, per rendere pigra questa funzione, è sufficiente restituire un proxy invece del risultato effettivo:

struct matrix_add;

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

Ora tutto ciò che deve essere fatto è scrivere questo 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 magia sta nel metodo operator matrix() che è un operatore di conversione implicito da matrix_add a semplice matrix. In questo modo, è possibile concatenare più operazioni (fornendo ovviamente sovraccarichi appropriati). La valutazione ha luogo solo quando il risultato finale è assegnato a un'istanza A.

MODIFICA Avrei dovuto essere più esplicito. Così com'è, il codice non ha senso perché, sebbene la valutazione avvenga pigramente, accade comunque nella stessa espressione. In particolare, un'altra aggiunta valuterà questo codice a meno che la struttura B non venga modificata per consentire l'aggiunta concatenata. C ++ 0x facilita notevolmente ciò consentendo modelli variadici (ovvero elenchi di modelli di lunghezza variabile).

Tuttavia, un caso molto semplice in cui questo codice avrebbe effettivamente un vantaggio reale e diretto è il seguente:

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

Qui, si presume che infix e <=> siano matrici bidimensionali e che la dereferenziazione sia effettuata nella notazione Fortran, ovvero quanto sopra calcola uno da una somma di matrice. Naturalmente è inutile aggiungere tutte le matrici. <=> in soccorso:

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

Altri esempi abbondano. Ho appena ricordato che ho implementato qualcosa di simile non molto tempo fa. Fondamentalmente, ho dovuto implementare una classe di stringhe che dovrebbe aderire a un'interfaccia fissa e predefinita. Tuttavia, la mia particolare classe di stringhe si occupava di stringhe enormi che non erano effettivamente archiviate in memoria. Di solito, l'utente accede semplicemente a piccole sottostringhe dalla stringa originale usando una funzione <=>. Ho sovraccaricato questa funzione per il mio tipo di stringa per restituire un proxy che conteneva un riferimento alla mia stringa, insieme alla posizione iniziale e finale desiderata. Solo quando questa sottostringa è stata effettivamente utilizzata, è stata eseguita una query su un'API C per recuperare questa parte della stringa.

Altri suggerimenti

Boost.Lambda è molto bello, ma Boost.Proto è esattamente quello che stai cercando. Ha già un sovraccarico di tutti operatori C ++, che per impostazione predefinita svolgono la loro solita funzione quando viene chiamato proto::eval(), ma possono essere modificati.

Ciò che Konrad ha già spiegato può essere ulteriormente sviluppato per supportare invocazioni nidificate di operatori, tutte eseguite pigramente. Nell'esempio di Konrad, ha un oggetto expression che può memorizzare esattamente due argomenti, per esattamente due operandi di una sola operazione. Il problema è che eseguirà pigramente una sottoespressione, il che spiega bene il concetto di valutazione pigra messo in termini semplici, ma non migliora sostanzialmente le prestazioni. L'altro esempio mostra anche come si può applicare operator() per aggiungere solo alcuni elementi usando quell'oggetto espressione. Ma per valutare espressioni complesse arbitrarie, abbiamo bisogno di alcuni meccanismi che possano archiviare anche la struttura di quello. Non possiamo aggirare i modelli per farlo. E il nome è expression templates. L'idea è che un oggetto espressione modello può memorizzare ricorsivamente la struttura di qualche sottoespressione arbitraria, come un albero, in cui le operazioni sono i nodi e gli operandi sono i nodi figlio. Per una molto buona spiegazione che ho appena trovato oggi (alcuni giorni dopo aver scritto il codice seguente) vedere qui .

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

Ciò memorizzerà qualsiasi operazione di aggiunta, anche nidificata, come si può vedere dalla seguente definizione di operatore + per un tipo di punto semplice:

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

Ora, se hai

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

Ora devi solo sovraccaricare operator = e aggiungere un costruttore adatto per il tipo Point e accettare AddOp. Cambia la sua definizione in:

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

E aggiungi get_x e get_y appropriati in AddOp come funzioni membro:

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

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

Nota come non abbiamo creato alcun temporaneo di tipo Point. Potrebbe essere stata una grande matrice con molti campi. Ma nel momento in cui è necessario il risultato, lo calcoliamo pigramente .

Non ho nulla da aggiungere al post di Konrad, ma puoi guardare Eigen per un esempio di valutazione pigra eseguita correttamente, in un'app del mondo reale. È davvero impressionante.

Sto pensando di implementare una classe modello che utilizza std::function. La classe dovrebbe apparire più o meno così:

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

Ad esempio utilizzo:

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

Il codice sopra riportato dovrebbe produrre il seguente messaggio sulla console:

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

Si noti che la funzione di stampa del costruttore Noisy(10) non verrà chiamata fino a quando non si accede alla variabile.

Questa classe è tutt'altro che perfetta, però. La prima cosa sarebbe il costruttore predefinito di Value dovrà essere chiamato durante l'inizializzazione dei membri (stampando Noisy(0) in questo caso). Possiamo invece usare il puntatore per _value, ma non sono sicuro che ciò influirebbe sulle prestazioni.

La risposta di Johannes funziona, ma quando si tratta di più parentesi, non funziona come desiderato. Ecco un esempio.

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

Perché i tre operatori + sovraccaricati non hanno coperto il caso

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

Quindi il compilatore deve convertire (p1 + p2) o (p3 + p4) in Point, non è abbastanza pigro e quando il compilatore decide quale convertire, si lamenta. Perché nessuno è migliore dell'altro. Ecco la mia estensione: aggiungi ancora un altro operatore sovraccarico +

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

}

Ora, il compilatore può gestire correttamente il caso sopra e nessuna conversione implicita, volia!

C ++ 0x è bello e tutto .... ma per quelli di noi che vivono nel presente hai la libreria Boost lambda e Boost Phoenix. Entrambi con l'intento di portare grandi quantità di programmazione funzionale in C ++.

Tutto è possibile.

Dipende esattamente da cosa intendi:

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

          return instanceA;
     }
};

Qui abbiamo l'effetto di una variabile globale che viene pigramente valutata al primo utilizzo.

Come richiesto di recente nella domanda.
E rubare il design di Konrad Rudolph ed estenderlo.

L'oggetto pigro:

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

Come si usa:

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

Come sarà fatto in C ++ 0x , dalle espressioni lambda.

In C ++ 11 una valutazione pigra simile alla risposta di hiapay può essere ottenuta usando std :: shared_future. Devi ancora incapsulare i calcoli in lambdas ma la memoization è curata:

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

Ecco un esempio completo:

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

Prendiamo Haskell come nostra fonte di ispirazione - essendo pigro fino al midollo. Inoltre, ricordiamo come Linq in C # usa gli enumeratori in modo monadico (urgh - ecco la parola - scusa). Infine, ricordiamo ciò che le coroutine dovrebbero fornire ai programmatori. Vale a dire il disaccoppiamento delle fasi computazionali (ad esempio produttore consumatore) l'una dall'altra. E proviamo a pensare a come le coroutine si collegano alla valutazione pigra.

Tutto quanto sopra sembra essere in qualche modo correlato.

Quindi, proviamo a estrarre la nostra definizione personale di cosa " lazy " scende a.

Un'interpretazione è: vogliamo dichiarare il nostro calcolo in modo componibile, prima di eseguirlo. Alcune di quelle parti che usiamo per comporre la nostra soluzione completa potrebbero benissimo attingere a fonti di dati enormi (a volte infinite), con il nostro calcolo completo che può anche produrre un risultato finito o infinito.

Consente di ottenere concreti e in un po 'di codice. Abbiamo bisogno di un esempio per questo! Qui, scelgo il fizzbuzz & Quot; problema & Quot; ad esempio, solo per il motivo che esiste una soluzione piacevole e pigra.

In Haskell, assomiglia a questo:     

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

La funzione Haskell cycle crea un elenco infinito (pigro, ovviamente!) da un elenco finito semplicemente ripetendo i valori nell'elenco finito per sempre. In uno stile di programmazione entusiasta, scrivere qualcosa del genere suonerebbe campanelli d'allarme (overflow di memoria, loop infiniti!). Ma non così in una lingua pigra. Il trucco è che gli elenchi pigri non vengono calcolati immediatamente. Forse mai. Normalmente solo quanto il codice successivo lo richiede.

La terza riga nel blocco where sopra crea un altro pigro !! list, mediante la combinazione degli infiniti elenchi fizz e buzz mediante la ricetta a due elementi singoli " concatenare un elemento stringa da uno degli elenchi di input in una singola stringa " ;. Ancora una volta, se questo dovesse essere immediatamente valutato, dovremmo aspettare che le risorse del nostro computer finiscano.

Nella 4a riga, creiamo tuple dei membri di una lista pigra finita [1..n] con la nostra lista pigra infinita fizzbuzz. Il risultato è ancora pigro.

Anche nel corpo principale della nostra funzione fb, non c'è bisogno di diventare desiderosi. L'intera funzione restituisce un elenco con la soluzione, che di per sé è -again- pigra. Potresti anche pensare al risultato di fb 50 come un calcolo che puoi (parzialmente) valutare in seguito. O combinalo con altre cose, portando a una valutazione ancora più ampia (pigra).

Quindi, per iniziare con la nostra versione C ++ di " fizzbuzz " ;, dobbiamo pensare a come combinare i passaggi parziali del nostro calcolo in bit più grandi di calcoli, ogni dato di disegno dai passaggi precedenti come richiesto.

Puoi vedere la storia completa in una mia idea .

Ecco le idee di base dietro il codice:

Prendendo in prestito da C # e Linq, noi " inventiamo " un tipo generico con stato Enumerator, che contiene
  - Il valore corrente del calcolo parziale
  - Lo stato di un calcolo parziale (in modo da poter produrre valori successivi)
  - La funzione worker, che produce lo stato successivo, il valore successivo e un valore booleano che indica se ci sono più dati o se l'enumerazione è terminata.

Per poter comporre Enumerator<T,S> istanza mediante la potenza del . (punto), questa classe contiene anche funzioni, prese in prestito da classi di tipo Haskell come Functor e Applicative.

La funzione di lavoro per l'enumeratore ha sempre la forma: S -> std::tuple<bool,S,T dove S è la variabile di tipo generico che rappresenta lo stato e T è la variabile di tipo generico che rappresenta un valore - il risultato di una fase di calcolo.

Tutto questo è già visibile nelle prime righe della range(first,last) definizione della classe.

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{}
    {
    }
    // ...
};

Quindi, tutto ciò di cui abbiamo bisogno per creare una specifica istanza di enumeratore, dobbiamo creare un fu di lavoronction, avere lo stato iniziale e creare un'istanza di auto r1 = range(size_t{1},10); con questi due argomenti.

Ecco un esempio: la funzione eternally crea un intervallo finito di valori. Ciò corrisponde a un elenco pigro nel mondo di Haskell.

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

E possiamo fare uso di questa funzione, ad esempio in questo modo: auto foo = cycle(range(size_t{1},3)); - Abbiamo creato un elenco pigro con 10 elementi!

Ora, manca tutto per il nostro " wow " esperienza, è vedere come possiamo comporre gli enumeratori. Tornando alla funzione di Haskells zip, che è piuttosto interessante. Come sarebbe nel nostro mondo C ++? Eccolo:

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

Prende un enumeratore come input e restituisce un enumeratore. La funzione locale (lambda) class Enumerator reimposta semplicemente l'enumerazione di input al suo valore iniziale ogni volta che si esauriscono i valori e voil & # 224; - abbiamo una versione infinita e ripetuta dell'elenco che abbiamo fornito come argomento :: fizzes E possiamo già comporre spudoratamente il nostro pigro " calcoli " ;.

buzzes è un buon esempio, dimostrando che possiamo anche creare un nuovo enumeratore da due enumeratori di input. L'enumeratore risultante produce tanti valori quanti sono i più piccoli di uno degli enumeratori di input (tuple con 2 elementi, uno per ciascun enumeratore di input). Ho implementato iterRange(..) dentro <=> stesso. Ecco come appare:

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

Nota, come " combinando " finisce anche per combinare lo stato di entrambe le fonti e i valori di entrambe le fonti.

Dato che questo post è già TL; DR; per molti, qui il ...

Riepilogo

Sì, la valutazione lazy può essere implementata in C ++. Qui l'ho fatto prendendo in prestito i nomi delle funzioni da haskell e il paradigma dagli enumeratori C # e Linq. Potrebbero esserci delle somiglianze con i pitoni itertools, tra l'altro. Penso che abbiano seguito un approccio simile.

La mia implementazione (vedere il link gist sopra) è solo un prototipo, non un codice di produzione, tra l'altro. Quindi nessuna garanzia da parte mia. Serve anche come codice demo per far passare l'idea generale.

E quale sarebbe questa risposta senza la versione C ++ finale di fizzbuz, eh? Eccolo:

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

E ... per portare il punto a casa ancora di più - qui una variazione di fizzbuzz che restituisce un " infinito elenco " al chiamante:

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

Vale la pena mostrare, dal momento che puoi imparare da esso come schivare la domanda quale sia il tipo di ritorno esatto di quella funzione (poiché dipende solo dall'implementazione della funzione, vale a dire come il codice combina gli enumeratori).

Inoltre dimostra che abbiamo dovuto spostare i vettori <=> e <=> al di fuori dell'ambito della funzione in modo che siano ancora presenti quando eventualmente all'esterno, il meccanismo pigro produce valori. Se non lo avessimo fatto, il <=> codice avrebbe memorizzato gli iteratori nei vettori che sono scomparsi da tempo.

Usando una definizione molto semplice di valutazione pigra, che è il valore che non viene valutato fino al momento necessario, direi che si potrebbe implementare questo attraverso l'uso di un puntatore e macro (per lo zucchero di sintassi).

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

Noterai nella funzione print_power2 che esiste una macro chiamata eval_lazy che non fa altro che dereferenziare un puntatore per ottenere il valore appena prima di quando è effettivamente necessario. Il tipo pigro è accessibile atomicamente, quindi è completamente thread-safe.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top