Чистый способ ленивой инициализации и кэширования внутреннего значения в лямбде - PullRequest
0 голосов
/ 18 января 2019

Пусть код говорит сам за себя первым с наивным подходом:

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

Я хочу, чтобы внутреннее кэшированное значение лямбды вычислялось при первом вызове.Наивным подходом является использование static кеша, но он увеличивает двоичный размер и отказывается быть встроенным .

Я придумал создать cache в списке захвата и пометить лямбду какmutable, что встраивает без проблем , но требует, чтобы кэш начинался со значения по умолчанию, что может нарушить инвариант класса.


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

Мой третий подход использует boost::optional вmutable lambda

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

Работает нормально, но выглядит как своего рода список захвата + mutable взлом ключевого слова.Также mutable влияет на все записанные параметры, поэтому делает лямбду менее безопасной в реальном использовании.

Может быть, есть лучшее / более чистое решение для этого?Или просто другой подход, который приводит к тому же самому эффекту.

РЕДАКТИРОВАТЬ, некоторая предыстория: Лямбда-подход выбран, поскольку я изменяю некоторую лямбду обратного вызова, которая в настоящее время используется как: [this, param]{this->onEvent(heavy_calc(param));} Я хочу уменьшить heavy_calc звонки без предварительной оценки (только при первом звонке)

Ответы [ 3 ]

0 голосов
/ 18 января 2019

Это работает правильно, но выглядит для меня как список перехвата + изменяемое ключевое слово hack. Изменяемый также влияет на все записанные параметры, что делает лямбда менее безопасным при реальном использовании.

Существует решение для рулонной собственной лямбды ручной работы:

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

Он встроен таким же образом, и вам не нужно полагаться на захват + изменяемый хак.

0 голосов
/ 18 января 2019

Я верю, что это именно тот случай использования изменяемой лямбды. Если вы не хотите, чтобы все переменные были изменяемыми, я предлагаю просто создать класс функтора с одним полем mutable. Таким образом, вы получаете лучшее из обоих миров (хорошо, это не так кратко). Дополнительным преимуществом является то, что operator() равно const (что совершенно верно, поскольку оно всегда возвращает одно и то же значение)

#include <optional>

int heavy_calc() {
    // sleep(7500000 years)
    return 42;
}
struct my_functor {
    mutable std::optional<int> cache;
    int operator()() const {
        if (!cache) cache = heavy_calc();
        return *cache;
    }
}

int main() {
    my_functor foo;
    return foo() + foo();
}
0 голосов
/ 18 января 2019

Если честно, я не вижу никакой причины использовать лямбду здесь. Вы можете написать обычный класс многократного использования для кэширования значения вычисления. Если вы настаиваете на использовании лямбды, то можете перенести вычисление значений в параметры, чтобы вам не нужно было ничего делать 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();
}

онлайн-компилятор

С небольшим количеством шаблона можно написать класс, который будет лениво оценивать и кэшировать результат произвольного вычисления:

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

онлайн-компилятор

...