9
votes

Méthode propre pour initialiser paresseusement et mettre en cache la valeur interne dans lambda

Laissez le code parler de lui-même en premier avec une approche naïve:

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

Je souhaite que la valeur de mise en cache interne lambda soit calculée au premier appel. Une approche naïve consiste à utiliser le cache statique , mais cela augmente la taille du binaire et refuse d'être intégré .

J'ai créé un cache dans la liste de capture et le marquage de lambda comme mutable , quoi en ligne sans problème , mais nécessite que le cache commence avec la valeur par défaut, ce qui peut casser l'invariant de classe.

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

Ma troisième approche utilise boost :: optional dans le code lambda mutable >

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

Cela fonctionne correctement, mais me recherche comme une sorte de liste de capture + hack de mot-clé mutable . mutable affecte également tous les paramètres capturés, ce qui rend lambda moins sûr en utilisation réelle.

Peut-être existe-t-il une solution meilleure / plus propre pour cela? Ou juste une approche différente qui aboutit au même effet.

EDIT, quelques arrière-plans: L'approche Lambda est choisie car je modifie un lambda de rappel, qui est actuellement utilisé comme: [this, param] {this-> onEvent (heavy_calc (param));} Je souhaite réduire les appels heavy_calc sans l'évaluer à l'avance (uniquement lors du premier appel)


4 commentaires

heavy_calc devrait être appelé une fois à l'intérieur du lambda ou peut-il être appelé à l'extérieur? (la question est posée pour des raisons de sécurité des threads)


J'adorerais l'appeler seulement à l'intérieur. Je n'ai pas pris en compte la sécurité des threads, mais une autre solution naïve avec la sécurité des threads serait thread_local au lieu de static , non?


votre réponse avec statique est correcte du point de vue de la sécurité des threads. En C ++ 11 d'après ce que je sais, vous avez la garantie d'une initialisation thread-safe de la variable statique. Une autre option consisterait à utiliser call_once en.cppreference.com/w/cpp/thread/call_once < / a> mais j'irais avec statique.


Donc, thread_local et static sont ok. Ma question sur la sécurité des threads portait sur le thread à appeler heavy_calc. Et vous avez répondu qu'il devrait être appelé à partir du thread qui appelle la première fonction lambda.


3 Réponses :


5
votes

Pour être honnête, je ne vois aucune raison d'utiliser lambda ici. Vous pouvez écrire une classe réutilisable régulière pour mettre en cache la valeur de calcul. Si vous insistez pour utiliser lambda, vous pouvez déplacer le calcul de la valeur vers les paramètres afin qu'il n'y ait pas besoin de rendre quoi que ce soit mutable:

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

compilateur en ligne

Avec un peu de template, il est possible d'écrire une classe qui évaluera paresseusement et mettra en cache le résultat de calcul arbitraire:

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

compilateur en ligne p>


11 commentaires

Je pense que l'idée est de ne pas appeler heavy_calc () si ce n'est pas nécessaire, par exemple, si vous avez des branches qui peuvent ne pas utiliser foo . Sinon, je suis complètement d'accord sur la classe de cache réutilisable.


Quand cache = heavy_calc () est-il réellement exécuté dans ce cas?


@GPhilo Quand lambda est construit


J'ai simplifié le cas ici, je modifie le rappel lambda existant qui fait essentiellement actuellement: [this, param] {this-> call (heavy calc (param));}


@ R2RT Votre objectif principal est-il donc de réaliser une évaluation paresseuse? Cette question serait quelque peu différente de la simple mise en cache (bien que les classes réutilisables régulières fonctionneraient également).


Oui, je l'ai voulu dire dans la première phrase "calculé au premier appel". J'ai ajouté une modification pour la rendre plus explicite en question.


@VTT Donc, dans votre retour , vous appelleriez heavy_calc deux fois à cause du double foo ? Ou le lambda est-il construit une seule fois lorsque foo est défini puis appelé implicitement par opérateur () de foo ? (Aussi, qu'est-ce que foo exactement? Une classe sans nom? Une sorte de fonction? Je n'ai jamais imaginé une telle utilisation de auto auparavant)


@GPhilo foo est une instance de classe appelable anonyme, heavy_calc ne sera invoqué qu'une seule fois lorsque foo est construit.


@VTT merci pour la clarification, sauriez-vous peut-être aussi où obtenir plus d'informations sur les classes appelables anonymes? (Google "classe appelable anonyme c ++" et "classe appelable c ++" ne semble pas retourner quelque chose de similaire, mais peut-être que je manque juste quelque chose)


@papagaga Je connais les lambdas, ce qui me déroute, c'est la construction auto name {lambda_def}; . Serait-ce équivalent à auto foo = lambda_def; ?


@GPhilo: alors vous devez jeter un œil à l'initialisation des accolades



2
votes

Cela fonctionne correctement, mais me recherche comme une sorte de liste de capture + piratage de mot-clé mutable. La mutation affecte également tous les paramètres capturés, ce qui rend lambda moins sûr en utilisation réelle.

Il existe la solution pour rouler votre propre lambda fait à la main:

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

Il est intégré de la même manière et vous n'avez pas besoin de vous fier à capture + mutable pirater.


0 commentaires

0
votes

Je pense que c'est exactement le cas d'utilisation de lambda mutable. Si vous ne voulez pas que toutes les variables soient mutables, je suggère simplement de créer une classe de foncteurs avec un champ mutable . De cette façon, vous obtenez le meilleur des deux mondes (ok, ce n'est pas si concis). L'avantage supplémentaire est que operator () est const (ce qui est tout à fait exact, car il renvoie toujours la même valeur)

#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 commentaires