Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clean way to lazy initialize and cache internal value in lambda

Tags:

c++

lambda

c++14

Let the code speak for itself first with naive approach:

int heavy_calc() // needed to be called once
{
    // sleep(7500000 years)
    return 42;
}

int main()
{
    auto foo = [] {
        // And cached for lambda return value
        static int cache = heavy_calc();
        return cache;
    };
    return foo() + foo();
}

I want to have lambda internal cached value calculated on the first call. An naive approach is to use static cache, but it increases binary size and refuses to be be inlined.

I came up with creating cache in capture list and marking lambda as mutable, what inlines without problems, but requires cache to start with default value, which may break class invariant.


auto foo = [cache=0] () mutable {
    // And cached for lambda return value
    if(!cache)
        cache = heavy_calc();
    return cache;
};

My third approach uses boost::optional in mutable lambda

auto foo = [cache=std::optional<int>{}] () mutable {
    // And cached for lambda return value
    if(!cache)
        cache = heavy_calc();
    return *cache;
};

It works properly, but looks for me as kind of capture list + mutable keyword hack. Also mutable affects all captured parameters, so makes lambda less safe in real use.

Maybe there is an better/more clean solution for this? Or just different approach which ends up with the very same effect.

EDIT, some background: Lambda approach is chosen as I am modifying some callback lambda, which currently is used as: [this, param]{this->onEvent(heavy_calc(param));} I want to reduce heavy_calc calls without evaluating it in advance (only on first call)

like image 963
R2RT Avatar asked Jan 18 '19 09:01

R2RT


2 Answers

To be honest, I don't see any reason to use lambda here. You can write a regular reusable class to cache calculation value. If you insist on using lambda then you can move value calculation to parameters so there will be no need to make anything mutable:

int heavy_calc() // needed to be called once
{
    // sleep(7500000 years)
    return 42;
}

int main()
{
    auto foo
    {
        [cache = heavy_calc()](void)
        {
            return cache;
        }
    };
    return foo() + foo();
}

online compiler

With a bit of template it is possible to write a class that will lazy evaluate and cache result of arbitrary calculation:

#include <boost/optional.hpp>
#include <utility>

template<typename x_Action> class
t_LazyCached final
{
    private: x_Action m_action;
    private: ::boost::optional<decltype(::std::declval<x_Action>()())> m_cache;

    public: template<typename xx_Action> explicit
    t_LazyCached(xx_Action && action): m_action{::std::forward<xx_Action>(action)}, m_cache{} {}

    public: auto const &
    operator ()(void)
    {
        if(not m_cache)
        {
            m_cache = m_action();
        }
        return m_cache.value();
    }
};

template<typename x_Action> auto
Make_LazyCached(x_Action && action)
{
    return t_LazyCached<x_Action>{::std::forward<x_Action>(action)};
}

class t_Obj
{
    public: int heavy_calc(int param) // needed to be called once
    {
        // sleep(7500000 years)
        return 42 + param;
    }
};

int main()
{
    t_Obj obj{};
    int param{3};
    auto foo{Make_LazyCached([&](void){ return obj.heavy_calc(param); })};
    return foo() + foo();
}

online compiler

like image 173
user7860670 Avatar answered Oct 13 '22 13:10

user7860670


It works properly, but looks for me as kind of capture list + mutable keyword hack. Also mutable affects all captured parameters, so makes lambda less safe in real use.

There is the solution to roll your own, hand-made lambda:

#include <optional>

int heavy_calc() // needed to be called once
{
    // sleep(7500000 years)
    return 42;
}


int main()
{
    struct {
        std::optional<int> cache;
        int operator()() {
            if (!cache) cache = heavy_calc();
            return *cache;
        }
    } foo;
    return foo() + foo();
}

It's inlined the same way and you don't need to rely on the capture+mutable hack.

like image 2
papagaga Avatar answered Oct 13 '22 15:10

papagaga