Dois-je passer une fonction std :: function par const-reference?

141

Disons que j'ai une fonction qui prend un std::function:

void callFunction(std::function<void()> x)
{
    x();
}

Dois-je xplutôt passer par const-reference?:

void callFunction(const std::function<void()>& x)
{
    x();
}

La réponse à cette question change-t-elle en fonction de ce que la fonction en fait? Par exemple, s'il s'agit d'une fonction membre de classe ou d'un constructeur qui stocke ou initialise le std::functiondans une variable membre.

Sven Adbring
la source
1
Probablement pas. Je ne sais pas avec certitude, mais je m'attendrais sizeof(std::function)à ne pas dépasser 2 * sizeof(size_t), ce qui est la plus petite taille que vous auriez jamais envisagée pour une référence const.
Mats Petersson
12
@Mats: Je ne pense pas que la taille du std::functionwrapper soit aussi importante que la complexité de sa copie. Si des copies complètes sont impliquées, cela pourrait être beaucoup plus cher que ce que sizeofsuggère.
Ben Voigt
Devriez-vous movela fonction?
Yakk - Adam Nevraumont
operator()()est constdonc une référence const devrait fonctionner. Mais je n'ai jamais utilisé std :: function.
Neel Basu
@Yakk Je viens de passer un lambda directement à la fonction.
Sven Adbring

Réponses:

79

Si vous voulez des performances, passez par valeur si vous les stockez.

Supposons que vous ayez une fonction appelée "exécuter ceci dans le thread de l'interface utilisateur".

std::future<void> run_in_ui_thread( std::function<void()> )

qui exécute du code dans le thread "ui", puis signale le futurequand c'est fait. (Utile dans les frameworks d'interface utilisateur où le thread d'interface utilisateur est l'endroit où vous êtes censé jouer avec les éléments de l'interface utilisateur)

Nous avons deux signatures que nous envisageons:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

Maintenant, nous sommes susceptibles de les utiliser comme suit:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

qui va créer une fermeture anonyme (un lambda), en construire une std::function, la transmettre à la run_in_ui_threadfonction, puis attendre qu'elle se termine dans le thread principal.

Dans le cas (A), le std::functionest directement construit à partir de notre lambda, qui est ensuite utilisé dans le run_in_ui_thread. Le lambda est moved dans le std::function, de sorte que tout état mobile y est efficacement transporté.

Dans le second cas, un temporaire std::functionest créé, le lambda est moved dedans, puis ce temporaire std::functionest utilisé par référence dans le run_in_ui_thread.

Jusqu'à présent, tout va bien - les deux fonctionnent de la même manière. Sauf que run_in_ui_threadva faire une copie de son argument de fonction à envoyer au thread ui pour l'exécuter! (il reviendra avant d'en avoir terminé, il ne peut donc pas simplement y faire référence). Dans le cas (A), nous avons simplement movela std::functiondans son stockage à long terme. Dans le cas (B), nous sommes obligés de copier le fichier std::function.

Ce magasin rend le passage de valeur plus optimal. S'il y a une possibilité que vous stockiez une copie de std::function, passez par valeur. Sinon, les deux méthodes sont à peu près équivalentes: le seul inconvénient de la valeur par valeur est que vous prenez le même encombrant std::functionet que vous utilisez une sous-méthode après une autre. Sauf cela, un movesera aussi efficace qu'un const&.

Maintenant, il y a d'autres différences entre les deux qui interviennent principalement si nous avons un état persistant dans le std::function.

Supposons que le std::functionstocke un objet avec un operator() const, mais qu'il a également mutabledes membres de données qu'il modifie (c'est grossier!).

Dans ce std::function<> const&cas, les mutablemembres de données modifiés se propageront hors de l'appel de fonction. Dans le std::function<>cas, ils ne le feront pas.

C'est un cas d'angle relativement étrange.

Vous voulez traiter std::functioncomme vous le feriez pour n'importe quel autre type mobile éventuellement lourd et bon marché. Le déménagement est bon marché, la copie peut coûter cher.

Yakk - Adam Nevraumont
la source
L'avantage sémantique de "passer par valeur si vous le stockez", comme vous le dites, est que par contrat la fonction ne peut pas garder l'adresse de l'argument passé. Mais est-il vraiment vrai que "Sauf cela, un mouvement sera aussi efficace qu'un const &"? Je vois toujours le coût d'une opération de copie plus le coût de l'opération de déplacement. Au passage const&je ne vois que le coût de l'opération de copie.
ceztko
2
@ceztko Dans les deux cas (A) et (B), le temporaire std::functionest créé à partir du lambda. Dans (A), le temporaire est élidé dans l'argument de run_in_ui_thread. En (B), une référence audit temporaire est transmise run_in_ui_thread. Tant que vos std::functions sont créés à partir de lambdas en tant que temporaires, cette clause est valable. Le paragraphe précédent traite du cas où le std::functionpersiste. Si nous ne stockons pas , nous créons simplement à partir d'un lambda function const&et functionavons exactement la même surcharge.
Yakk - Adam Nevraumont
Ah, je vois! Cela dépend bien sûr de ce qui se passe en dehors de run_in_ui_thread(). Y a-t-il juste une signature pour dire "Passer par référence, mais je ne conserverai pas l'adresse"?
ceztko
@ceztko Non, il n'y en a pas.
Yakk - Adam Nevraumont
1
@ Yakk-AdamNevraumont s'il serait plus complet de couvrir une autre option pour passer par rvalue ref:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P
33

Si vous êtes préoccupé par les performances et que vous ne définissez pas de fonction membre virtuelle, vous ne devriez probablement pas utiliser std::functiondu tout.

Faire du type de foncteur un paramètre de modèle permet une plus grande optimisation que std::function, y compris l'intégration de la logique du foncteur. L'effet de ces optimisations est susceptible de l'emporter largement sur les préoccupations de copie contre indirection sur la façon de passer std::function.

Plus rapide:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}
Ben Voigt
la source
1
Je ne suis pas du tout préoccupé par les performances en fait. Je pensais juste que l'utilisation de références const là où elles devraient être utilisées était une pratique courante (les chaînes et les vecteurs me viennent à l'esprit).
Sven Adbring
13
@Ben: Je pense que le moyen le plus moderne d'implémenter cela est d'utiliser std::forward<Functor>(x)();, pour préserver la catégorie de valeur du foncteur, car c'est une référence "universelle". Cela ne fera pas de différence dans 99% des cas, cependant.
GManNickG
1
@Ben Voigt donc pour votre cas, est-ce que j'appellerais la fonction avec un mouvement? callFunction(std::move(myFunctor));
arias_JC
2
@arias_JC: Si le paramètre est un lambda, c'est déjà une rvalue. Si vous avez une lvalue, vous pouvez soit utiliser std::movesi vous n'en aurez plus besoin d'une autre manière, soit passer directement si vous ne voulez pas sortir de l'objet existant. Les règles de réduction des références garantissent que le callFunction<T&>()paramètre a un type T&, non T&&.
Ben Voigt
1
@BoltzmannBrain: J'ai choisi de ne pas faire ce changement car il n'est valable que pour le cas le plus simple, lorsque la fonction n'est appelée qu'une seule fois. Ma réponse est à la question "comment dois-je passer un objet fonction?" et pas limité à une fonction qui ne fait rien d'autre que d'invoquer inconditionnellement ce foncteur exactement une fois.
Ben Voigt
25

Comme d'habitude dans C ++ 11, le passage par valeur / référence / const-reference dépend de ce que vous faites avec votre argument. std::functionn'est pas différent.

Le passage par valeur vous permet de déplacer l'argument dans une variable (généralement une variable membre d'une classe):

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

Lorsque vous savez que votre fonction déplacera son argument, c'est la meilleure solution, de cette façon vos utilisateurs peuvent contrôler la façon dont ils appellent votre fonction:

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

Je crois que vous connaissez déjà la sémantique des références (non) const, donc je ne vais pas insister sur ce point. Si vous avez besoin que j'ajoute plus d'explications à ce sujet, demandez simplement et je mettrai à jour.

syam
la source