web-dev-qa-db-fra.com

Pourquoi le lambda de C ++ 11 requiert-il un mot clé "mutable" pour la capture par valeur, par défaut?

Petit exemple:

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

La question: Pourquoi avons-nous besoin du mot clé mutable? C'est assez différent du passage de paramètre traditionnel aux fonctions nommées. Quelle est la raison derrière?

J'avais l'impression que le but de la capture par valeur est de permettre à l'utilisateur de modifier le temporaire, sinon je suis presque toujours mieux d'utiliser la capture par référence, n'est-ce pas?

Des éclaircissements?

(J'utilise MSVC2010 en passant. Autant que je sache, cela devrait être standard)

238
kizzx2

Il nécessite mutable car, par défaut, un objet fonction devrait produire le même résultat à chaque appel. C'est la différence entre une fonction orientée objet et une fonction utilisant une variable globale, de manière efficace.

217
Puppy

Votre code est presque équivalent à ceci:

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

Ainsi, vous pourriez penser à lambdas comme générant une classe avec operator () dont la valeur par défaut est const, à moins que vous ne disiez qu’elle est mutable.

Vous pouvez également considérer toutes les variables capturées dans [] (explicitement ou implicitement) comme des membres de cette classe: des copies des objets pour [=] ou des références aux objets pour [&]. Ils sont initialisés lorsque vous déclarez votre lambda comme s'il y avait un constructeur caché.

99
Daniel Munoz

J'avais l'impression que le but de la capture par valeur est de permettre à l'utilisateur de modifier le temporaire, sinon je suis presque toujours mieux d'utiliser la capture par référence, n'est-ce pas?

La question est, est-ce "presque"? Un cas d'utilisation fréquent semble être de retourner ou de passer des lambdas:

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

Je pense que mutable n'est pas un cas de "presque". Je considère "capture par valeur" comme "permettez-moi d'utiliser sa valeur après le décès de l'entité capturée" plutôt que "permettez-moi de changer une copie de celle-ci". Mais peut-être que cela peut être discuté.

35

FWIW, Herb Sutter, membre bien connu du comité de normalisation C++, apporte une réponse différente à cette question dans Problèmes de correction lambda et d’utilisabilité :

Prenons cet exemple d'homme de paille, dans lequel le programmeur capture une variable locale par valeur et tente de modifier la valeur capturée (qui est une variable membre de l'objet lambda):

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

Cette fonctionnalité semble avoir été ajoutée pour éviter que l’utilisateur ne réalise pas qu’il en a reçu une copie, et en particulier parce que les lambdas sont copiables, il est possible qu’il change une copie différente de lambda.

Son article explique pourquoi cela devrait être changé en C++ 14. Il est court, bien écrit, il vaut la peine de le lire si vous voulez savoir "ce qui préoccupe [les membres du comité]" en ce qui concerne cette caractéristique particulière.

27
akim

Voir ce projet , sous 5.1.2 [expr.prim.lambda], sous-paragraphe 5:

Le type de fermeture d’une expression lambda a un opérateur d’appel de fonction en ligne public (13.5.4) dont les paramètres et le type de retour sont décrits par la clause parameter-declaration-clause et le type trailingreturn de l’expression lambda. Cet opérateur d'appel de fonction est déclaré const (9.3.1) si et seulement si la clause de déclaration de paramètre lambdaexpression n'est pas suivie de mutable.

Modifier le commentaire de litb: Peut-être ont-ils pensé à une capture par valeur afin que les modifications externes des variables ne soient pas reflétées dans le lambda? Les références fonctionnent dans les deux sens, c'est donc mon explication. Je ne sais pas si c'est bon quand même.

Edit sur le commentaire de kizzx2: La plupart du temps, lorsqu'un lambda doit être utilisé est un foncteur d'algorithmes. La valeur par défaut constness permet son utilisation dans un environnement constant, tout comme les fonctions normales const-qualifiées peuvent être utilisées ici, mais pas les fonctions non qualifiées -const-. Peut-être ont-ils simplement pensé à le rendre plus intuitif pour ces cas, qui savent ce qui se passe dans leur esprit. :)

15
Xeo

Vous devez penser à ce qu'est le type de fermeture de votre fonction Lambda. Chaque fois que vous déclarez une expression Lambda, le compilateur crée un type de fermeture, qui n'est rien de moins qu'une déclaration de classe sans nom avec des attributs ( environment où l'expression Lambda où déclaré) et l'appel de fonction ::operator() implémenté. Lorsque vous capturez une variable à l'aide de copie par valeur, le compilateur crée un nouvel attribut const dans le type de fermeture. Vous ne pouvez donc pas le modifier dans l'expression Lambda, car est un attribut "en lecture seule", c'est la raison pour laquelle ils l'appellent un "fermeture", car d'une manière ou d'une autre, vous fermez votre expression Lambda en copiant les variables de la portée supérieure dans la portée Lambda. Lorsque vous utilisez le mot-clé mutable, l'entité capturée devient un non-const attribut de votre type de fermeture. C'est ce qui fait que les modifications apportées à la variable mutable capturée par valeur ne sont pas propagées à la portée supérieure, mais restent à l'intérieur de la variable Lambda avec état. Essayez toujours d’imaginer le type de fermeture résultant de votre expression Lambda, cela m’a beaucoup aidé et j’espère que cela pourra vous aider aussi.

12
Tarantula

J'avais l'impression que le but de la capture par valeur est de permettre à l'utilisateur de modifier le temporaire, sinon je suis presque toujours mieux d'utiliser la capture par référence, n'est-ce pas?

n est pas temporaire. n est un membre de l'objet fonction-lambda que vous créez avec l'expression lambda. L'attente par défaut est que l'appel de votre lambda ne modifie pas son état, il est donc const pour vous empêcher de modifier accidentellement n.

10
Martin Ba

Il existe maintenant une proposition visant à réduire le besoin de mutable dans les déclarations lambda: n3424

4
usta

Vous devez comprendre ce que signifie capture! c'est capturer pas argument en passant! Regardons quelques exemples de code:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

Comme vous pouvez le constater même si x a été remplacé par 20 le lambda retourne toujours 10 (x est toujours 5 à l'intérieur du lambda) Changer x à l'intérieur du lambda signifie changer le lambda lui-même à chaque appel (le lambda est en mutation à chaque appel). Pour imposer l'exactitude, la norme a introduit le mot clé mutable. En spécifiant un lambda comme étant mutable, vous dites que chaque appel du lambda peut entraîner un changement du lambda lui-même. Voyons un autre exemple:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

L'exemple ci-dessus montre qu'en rendant le lambda mutable, changer x à l'intérieur du lambda "transforme" le lambda à chaque appel avec une nouvelle valeur de x qui n'a rien à voir avec la valeur réelle. de x dans la fonction principale

3
Soulimane Mammar

Pour prolonger la réponse de Puppy, les fonctions lambda sont destinées à être fonctions pures . Cela signifie que chaque appel auquel est attribué un jeu d'entrées unique renvoie toujours la même sortie. Définissons l'entrée comme l'ensemble de tous les arguments plus toutes les variables capturées lors de l'appel du lambda.

Dans les fonctions pures, la sortie dépend uniquement de l'entrée et non d'un état interne. Par conséquent, toute fonction lambda, si pure, n'a pas besoin de changer d'état et est donc immuable.

Quand un lambda capture par référence, écrire sur des variables capturées est une contrainte sur le concept de fonction pure, car une fonction pure ne doit que renvoyer une sortie, bien que le lambda ne mue certainement pas car l'écriture se produit sur des variables externes. Même dans ce cas, une utilisation correcte implique que si le lambda est appelé à nouveau avec la même entrée, la sortie sera toujours la même, malgré ces effets secondaires sur les variables de référence. De tels effets secondaires ne sont que des moyens de renvoyer une entrée supplémentaire (par exemple, mettre à jour un compteur) et pourraient être reformulés en une fonction pure, par exemple renvoyer un tuple au lieu d'une valeur unique.

0
Attersson