Pregunta

C++ no tiene soporte nativo para la evaluación diferida (como sí lo tiene Haskell).

Me pregunto si es posible implementar una evaluación diferida en C++ de manera razonable.Si es así, ¿cómo lo harías?

EDITAR:Me gusta la respuesta de Konrad Rudolph.

Me pregunto si es posible implementarlo de una manera más genérica, por ejemplo, usando una clase parametrizada lazy que esencialmente funciona para T de la misma manera que Matrix_add funciona para Matrix.

Cualquier operación en T volvería perezosa.El único problema es almacenar los argumentos y el código de operación dentro del propio lazy.¿Alguien puede ver cómo mejorar esto?

¿Fue útil?

Solución

Me pregunto si es posible implementar una evaluación diferida en C++ de manera razonable.Si es así, ¿cómo lo harías?

Sí, esto es posible y se hace con bastante frecuencia, p.para cálculos matriciales.El principal mecanismo para facilitar esto es la sobrecarga del operador.Considere el caso de la suma de matrices.La firma de la función normalmente se vería así:

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

Ahora, para hacer que esta función sea diferida, basta con devolver un proxy en lugar del resultado real:

struct matrix_add;

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

Ahora todo lo que hay que hacer es escribir este 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 está en el método. operator matrix() que es un operador de conversión implícito de matrix_add a llano matrix.De esta manera, puede encadenar múltiples operaciones (proporcionando las sobrecargas adecuadas, por supuesto).La evaluación tiene lugar sólo cuando el resultado final se asigna a un matrix instancia.

EDITAR Debería haber sido más explícito.Tal como están las cosas, el código no tiene sentido porque, aunque la evaluación se realiza de manera lenta, todavía ocurre en la misma expresión.En particular, otra adición evaluará este código a menos que el matrix_add La estructura se cambia para permitir la suma encadenada.C++ 0x facilita enormemente esto al permitir plantillas variadas (es decir,listas de plantillas de longitud variable).

Sin embargo, un caso muy simple en el que este código realmente tendría un beneficio real y directo es el siguiente:

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

Aquí se supone que A y B son matrices bidimensionales y la desreferenciación se realiza en notación Fortran, es decirlo anterior calcula uno elemento de una suma matricial.Por supuesto, es un desperdicio sumar las matrices completas. matrix_add al rescate:

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

Otros ejemplos abundan.Acabo de recordar que implementé algo relacionado no hace mucho.Básicamente, tuve que implementar una clase de cadena que debería adherirse a una interfaz fija y predefinida.Sin embargo, mi clase de cadena particular se ocupaba de cadenas enormes que en realidad no estaban almacenadas en la memoria.Por lo general, el usuario simplemente accedería a pequeñas subcadenas de la cadena original usando una función infix.Sobrecargué esta función para mi tipo de cadena para devolver un proxy que contenía una referencia a mi cadena, junto con la posición inicial y final deseada.Sólo cuando esta subcadena se usó realmente consultó una API de C para recuperar esta parte de la cadena.

Otros consejos

Boost.Lambda es muy agradable, pero Boost.Proto es exactamente lo que estás buscando. Ya tiene sobrecargas de todos operadores C ++, que por defecto realizan su función habitual cuando se llama a proto::eval(), pero se puede cambiar.

Lo que Konrad ya explicó se puede ampliar para admitir invocaciones anidadas de operadores, todo ejecutado perezosamente. En el ejemplo de Konrad, tiene un objeto de expresión que puede almacenar exactamente dos argumentos, para exactamente dos operandos de una operación. El problema es que solo ejecutará la subexpresión one perezosamente, lo que explica muy bien el concepto de evaluación perezosa en términos simples, pero no mejora sustancialmente el rendimiento. El otro ejemplo también muestra cómo se puede aplicar operator() para agregar solo algunos elementos usando ese objeto de expresión. Pero para evaluar expresiones complejas arbitrarias, necesitamos algún mecanismo que pueda almacenar la estructura de eso también. No podemos evitar las plantillas para hacer eso. Y el nombre para eso es expression templates. La idea es que un objeto de expresión con plantilla puede almacenar la estructura de alguna subexpresión arbitraria de forma recursiva, como un árbol, donde las operaciones son los nodos y los operandos son los nodos secundarios. Para una muy buena explicación que acabo de encontrar hoy (algunos días después de escribir el código a continuación) vea aquí .

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

Eso almacenará cualquier operación de suma, incluso una anidada, como se puede ver en la siguiente definición de un operador + para un tipo de punto 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);
}

Ahora, si tienes

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

Ahora solo necesita sobrecargar operator = y agregar un constructor adecuado para el tipo Point y aceptar AddOp. Cambie su definición a:

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

Y agregue el get_x y get_y apropiados en AddOp como funciones miembro:

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

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

Tenga en cuenta que no hemos creado ningún temporario de tipo Point. Podría haber sido una gran matriz con muchos campos. Pero en el momento en que se necesita el resultado, lo calculamos perezosamente .

No tengo nada que agregar a la publicación de Konrad, pero puedes mirar Eigen para un ejemplo de evaluación perezosa bien hecha, en una aplicación del mundo real. Es bastante inspirador.

Estoy pensando en implementar una clase de plantilla que use std::function. La clase debería, más o menos, verse así:

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

Por ejemplo, uso:

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

El código anterior debería producir el siguiente mensaje en la consola:

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

Tenga en cuenta que el constructor que imprime Noisy(10) no se llamará hasta que se acceda a la variable.

Sin embargo, esta clase está lejos de ser perfecta. Lo primero sería que el constructor predeterminado de Value deberá llamarse en la inicialización del miembro (imprimiendo Noisy(0) en este caso). En su lugar, podemos usar el puntero para _value, pero no estoy seguro de si afectaría el rendimiento.

La respuesta de Johannes funciona, pero cuando se trata de más paréntesis, no funciona como se desea. Aquí hay un ejemplo.

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

Porque los tres operadores sobrecargados + no cubrieron el caso

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

Entonces el compilador tiene que convertir (p1 + p2) o (p3 + p4) a Point, eso no es lo suficientemente flojo. Y cuando el compilador decide qué convertir, se queja. Porque ninguno es mejor que el otro. Aquí viene mi extensión: agregue otro operador sobrecargado +

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

}

Ahora, el compilador puede manejar el caso anterior correctamente, y no hay conversión implícita, volia!

C ++ 0x es agradable y todo ... pero para aquellos de nosotros que vivimos en el presente, tenemos la biblioteca Boost lambda y Boost Phoenix. Ambos con la intención de llevar grandes cantidades de programación funcional a C ++.

Cualquier cosa es posible.

Depende exactamente de lo que quieres decir:

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

          return instanceA;
     }
};

Aquí tenemos el efecto de una variable global que se evalúa perezosamente en el punto de primer uso.

Como se solicitó recientemente en la pregunta.
Y robando el diseño de Konrad Rudolph y extendiéndolo.

El objeto perezoso:

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

Cómo usarlo:

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

Como se hará en C ++ 0x , por expresiones lambda.

En C ++ 11 se puede lograr una evaluación diferida similar a la respuesta de hiapay usando std :: shared_future. Todavía tiene que encapsular los cálculos en lambdas, pero la memorización se ocupa de:

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

Aquí hay un ejemplo 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;
}

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top