So I was answering a question about lazy evaluation (here, my answer is overkill for that case but the idea seems interesting) and it made me think about how lazy evaluation might be done in C++. I came up with a way but I wasn't sure of all the pitfalls in this. Are there other ways of achieving lazy evaluation? how might this be done? What are the pitfalls and this and other designs?
Here is my idea:
#include <iostream>
#include <functional>
#include <memory>
#include <string>
#define LAZY(E) lazy<decltype((E))>{[&](){ return E; }}
template<class T>
class lazy {
private:
typedef std::function<std::shared_ptr<T>()> thunk_type;
mutable std::shared_ptr<thunk_type> thunk_ptr;
public:
lazy(const std::function<T()>& x)
: thunk_ptr(
std::make_shared<thunk_type>([x](){
return std::make_shared<T>(x());
})) {}
const T& operator()() const {
std::shared_ptr<T> val = (*thunk_ptr)();
*thunk_ptr = [val](){ return val; };
return *val;
}
T& operator()() {
std::shared_ptr<T> val = (*thunk_ptr)();
*thunk_ptr = [val](){ return val; };
return *val;
}
};
void log(const lazy<std::string>& msg) {
std::cout << msg() << std::endl;
}
int main() {
std::string hello = "hello";
std::string world = "world";
auto x = LAZY((std::cout << "I was evaluated!\n", hello + ", " + world + "!"));
log(x);
log(x);
log(x);
log(x);
return 0;
}
Some things I was concerned about in my design.
What are your thoughts?
thunk_type
and reference to it as a separate objects. Right now copy of lazy<T>
will gain nothing from evaluation of origin. But in that case you'll get additional indirect access.Consider next modification:
template<typename F>
lazy(const F& x) :
thunk_ptr([&x,&this](){
T val = (*x)();
thunk_ptr = [val]() { return val; };
return val;
})
{}
Or alternative implementation might look like:
template<typename F>
auto memo(const F &x) -> std::function<const decltype(x()) &()> {
typedef decltype(x()) return_type;
typedef std::function<const return_type &()> thunk_type;
auto thunk_ptr = std::make_shared<thunk_type>();
auto *thunk_cptr = thunk_ptr.get();
// note that this lambda is called only from scope which holds thunk_ptr
*thunk_ptr = [thunk_cptr, &x]() {
auto val = std::move(x());
auto &thunk = *thunk_cptr;
thunk = [val]() { return val; };
// at this moment we can't refer to catched vars
return thunk();
};
return [thunk_ptr]() { return (*thunk_ptr)(); };
};