Pourquoi le lambda de C ++ 11 nécessite-t-il, par défaut, un mot clé «mutable» pour la capture par valeur?


256

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 mutablemot-clé? C'est assez différent du passage de paramètres traditionnel aux fonctions nommées. Quelle est la justification derrière?

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

Des éclaircissements?

(J'utilise MSVC2010 au fait. AFAIK cela devrait être standard)


101
Bonne question; même si je suis content que quelque chose soit finalement constpar défaut!
xtofl

3
Pas une réponse, mais je pense que c'est une chose sensée: si vous prenez quelque chose par valeur, vous ne devriez pas le changer juste pour vous sauver 1 copie dans une variable locale. Au moins, vous ne ferez pas l'erreur de changer n en remplaçant = par &.
stefaanv

8
@xtofl: Je ne suis pas sûr que ce soit bon, alors que tout le reste ne l'est pas constpar défaut.
kizzx2

8
@ Tamás Szelei: Pas pour commencer un argument, mais à mon humble avis le concept "facile à apprendre" n'a pas sa place dans le langage C ++, en particulier dans les temps modernes. Quoi qu'il en soit: P
kizzx2

3
"tout le point de capture par valeur est de permettre à l'utilisateur de changer le temporaire" - Non, le point est que le lambda peut rester valide au-delà de la durée de vie des variables capturées. Si les lambdas C ++ n'avaient qu'une capture par référence, ils seraient inutilisables dans bien trop de scénarios.
Sebastian Redl

Réponses:


230

Cela nécessite mutableparce que par défaut, un objet fonction doit 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, effectivement.


7
C'est un bon point. Je suis tout à fait d'accord. En C ++ 0x cependant, je ne vois pas vraiment comment la valeur par défaut aide à appliquer ce qui précède. Considérez que je suis du côté récepteur du lambda, par exemple je le suis void f(const std::function<int(int)> g). Comment suis-je assuré qu'il gest effectivement référentiellement transparent ? gLe fournisseur de peut avoir utilisé de mutabletoute façon. Donc je ne sais pas. D'un autre côté, si la valeur par défaut est non- const, et que les gens doivent ajouter constau lieu d' mutableobjets de fonction, le compilateur peut réellement appliquer la const std::function<int(int)>partie et fpeut maintenant supposer que gc'est constnon?
kizzx2

8
@ kizzx2: En C ++, rien n'est appliqué , seulement suggéré. Comme d'habitude, si vous faites quelque chose de stupide (exigence documentée de transparence référentielle et passez ensuite une fonction non-référentiellement transparente), vous obtenez tout ce qui vous arrive.
Puppy

6
Cette réponse m'a ouvert les yeux. Auparavant, je pensais que dans ce cas, lambda ne mute qu'une copie pour le "run" en cours.
Zsolt Szatmari

4
@ZsoltSzatmari Votre commentaire m'a ouvert les yeux! : -DI n'a pas compris le vrai sens de cette réponse avant d'avoir lu votre commentaire.
Jendas

5
Je ne suis pas d'accord avec la prémisse de base de cette réponse. C ++ n'a pas de concept de "fonctions devraient toujours retourner la même valeur" n'importe où ailleurs dans le langage. En tant que principe de conception, je suis d'accord , il est une bonne façon d'écrire une fonction, mais je ne pense pas qu'il retient l' eau comme la raison du comportement standard.
Ionoclast Brigham

103

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"
}

Vous pourriez donc penser à lambdas comme générant une classe avec operator () qui est par défaut const à moins que vous ne disiez qu'elle est mutable.

Vous pouvez également considérer toutes les variables capturées à l'intérieur de [] (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é.


5
Bien qu'une belle explication de ce à quoi ressemblerait un constou mutablelambda s'il était implémenté en tant que types définis par l'utilisateur équivalents, la question est (comme dans le titre et développée par OP dans les commentaires) pourquoi const est la valeur par défaut, donc cela ne répond pas.
underscore_d

36

J'avais l'impression que tout l'intérêt de la capture par valeur était de permettre à l'utilisateur de modifier le temporaire - sinon je ferais 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 revenir 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 ce mutablen'est pas un cas de "presque". Je considère que "capture par valeur" comme "me permet d'utiliser sa valeur après la mort de l'entité capturée" plutôt que "me permet d'en changer une copie". Mais peut-être que cela peut être soutenu.


2
Bon exemple. Il s'agit d'un cas d'utilisation très solide pour l'utilisation de la capture par valeur. Mais pourquoi est-il par défaut const? À quoi sert-il? mutablesemble hors de propos ici, quand constn'est pas la valeur par défaut dans "presque" (: P) tout le reste de la langue.
kizzx2

8
@ kizzx2: Je souhaite que ce constsoit la valeur par défaut, au moins les gens seraient obligés de considérer la const-exactitude: /
Matthieu M.

1
@ kizzx2 regardant les papiers lambda, il me semble qu'ils le font par défaut pour constqu'ils puissent l'appeler, que l'objet lambda soit ou non const. Par exemple, ils pourraient le passer à une fonction prenant un std::function<void()> const&. Pour permettre au lambda de modifier ses copies capturées, dans les documents initiaux, les données des membres de la fermeture étaient définies mutableautomatiquement en interne. Vous devez maintenant saisir manuellement mutablel'expression lambda. Je n'ai cependant pas trouvé de justification détaillée.
Johannes Schaub - litb


5
À ce stade, pour moi, la "vraie" réponse / justification semble être "ils n'ont pas réussi à contourner un détail d'implémentation": /
kizzx2

32

FWIW, Herb Sutter, membre bien connu du comité de normalisation C ++, fournit une réponse différente à cette question dans Lambda Correctness and Ergonomie Issues :

Prenons l'exemple de l'homme de paille, où le programmeur capture une variable locale par valeur et essaie 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 par souci que l'utilisateur ne puisse pas réaliser qu'il a obtenu une copie, et en particulier parce que les lambdas sont copiables, il pourrait changer une copie de lambda différente.

Son article explique pourquoi cela devrait être changé en C ++ 14. Il est court, bien écrit et mérite d'être lu si vous voulez savoir «ce qui est à l'esprit [des membres du comité]» en ce qui concerne cette caractéristique particulière.


16

Vous devez réfléchir au 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 ( environnement où l'expression Lambda a été déclarée) et l'appel de fonction ::operator()implémenté. Lorsque vous capturez une variable à l'aide de la copie par valeur , le compilateur crée un nouvel constattribut dans le type de fermeture, vous ne pouvez donc pas le modifier dans l'expression Lambda car il s'agit d'un attribut "en lecture seule", c'est la raison pour laquelle ils appelons cela une " fermeture ", car d'une certaine manière, vous fermez votre expression Lambda en copiant les variables de l'étendue supérieure dans l'étendue Lambda.mutable, l'entité capturée deviendra un non-constattribut 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 à l'étendue supérieure, mais restent à l'intérieur du Lambda avec état. Essayez toujours d'imaginer le type de fermeture résultant de votre expression Lambda, qui m'a beaucoup aidé, et j'espère que cela peut vous aider aussi.


14

Voir ce projet , sous 5.1.2 [expr.prim.lambda], 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 respectivement par la clause de déclaration de paramètre et le type de retour de piste de l'expression lambda. Cet opérateur d'appel de fonction est déclaré const (9.3.1) si et seulement si la clause-déclaration-paramètre de lambdaexpression n'est pas suivie par mutable.

Modifier le commentaire de litb: Peut-être qu'ils ont pensé à la capture par valeur afin que les changements extérieurs aux variables ne soient pas reflétés à l'intérieur du lambda? Les références fonctionnent dans les deux sens, c'est donc mon explication. Je ne sais pas si c'est bon.

Edit sur le commentaire de kizzx2: La plupart du temps quand un lambda doit être utilisé est comme foncteur d'algorithmes. Le constness par défaut permet de l'utiliser dans un environnement constant, tout comme les constfonctions normales qualifiées peuvent y être utilisées, mais pas les fonctions non constqualifiées. Peut-être qu'ils ont juste pensé à le rendre plus intuitif pour ces cas, qui savent ce qui se passe dans leur esprit. :)


C'est la norme, mais pourquoi l'ont-ils écrit de cette façon?
kizzx2

@ kizzx2: Mon explication se trouve directement sous cette citation. :) Cela se rapporte un peu à ce que litb dit sur la durée de vie des objets capturés, mais va également un peu plus loin.
Xeo

@Xeo: Oh oui, j'ai raté ça: P C'est aussi une autre bonne explication pour une bonne utilisation de la capture par valeur . Mais pourquoi devrait-il être constpar défaut? J'ai déjà une nouvelle copie, il semble étrange de ne pas me laisser le changer - surtout que ce n'est pas quelque chose de mal principalement - ils veulent juste que j'ajoute mutable.
kizzx2

Je crois qu'il y a eu une tentative de créer une nouvelle syntaxe de déclaration de fonction génrale, ressemblant beaucoup à un lambda nommé. Il était également censé résoudre d'autres problèmes en rendant tout const par défaut. Jamais terminé, mais les idées déteignaient sur la définition lambda.
Bo Persson

2
@ kizzx2 - Si nous pouvions tout recommencer, nous aurions probablement varcomme mot-clé pour permettre le changement et la constante comme valeur par défaut pour tout le reste. Maintenant, nous ne le faisons pas, nous devons donc vivre avec cela. OMI, C ++ 2011 est plutôt bien sorti, compte tenu de tout.
Bo Persson

11

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

nn'est pas temporaire. n est 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 constant de vous empêcher de le modifier accidentellement n.


1
L'ensemble de l'objet lambda est temporaire, ses membres ont également une durée de vie temporaire.
Ben Voigt

2
@Ben: IIRC, je faisais allusion au problème que lorsque quelqu'un dit "temporaire", je le comprends comme signifiant un objet temporaire sans nom , qui est le lambda lui-même, mais ses membres ne le sont pas. Et aussi que de "l'intérieur" du lambda, peu importe si le lambda lui-même est temporaire. En relisant la question, il semblerait que OP voulait simplement dire «n à l'intérieur de la lambda» quand il a dit «temporaire».
Martin Ba

6

Vous devez comprendre ce que signifie la capture! c'est capturer et non passer l'argument! 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 voir, même s'il xa été modifié, 20le lambda renvoie toujours 10 ( xest 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 mute à chaque appel). Pour appliquer l'exactitude, la norme a introduit le mutablemot - clé. En spécifiant un lambda comme mutable, vous dites que chaque appel au lambda pourrait provoquer un changement dans le 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 "mute" le lambda à chaque appel avec une nouvelle valeur xqui n'a rien à voir avec la valeur réelle de xdans la fonction principale


4

Il existe maintenant une proposition pour alléger le besoin de mutabledéclarations in lambda: n3424


Des informations sur ce qui est arrivé? Personnellement, je pense que c'est une mauvaise idée, car la nouvelle "capture d'expressions arbitraires" atténue la plupart des problèmes.
Ben Voigt

1
@BenVoigt Oui, cela semble être un changement pour le bien du changement.
Miles Rout

3
@BenVoigt Bien que pour être juste, je m'attends à ce qu'il y ait probablement de nombreux développeurs C ++ qui ne savent mutablemême pas que c'est même un mot clé en C ++.
Miles Rout

1

Pour étendre la réponse de Puppy, les fonctions lambda sont censées être des fonctions pures . Cela signifie que chaque appel donné un ensemble d'entrée 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 lorsque le lambda est appelé.

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 elle est pure, n'a pas besoin de changer son état et est donc immuable.

Lorsqu'un lambda capture par référence, l'écriture sur les variables capturées met à rude épreuve le concept de fonction pure, car tout ce qu'une fonction pure devrait faire est de renvoyer une sortie, bien que le lambda ne mute 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 la même à chaque fois, malgré ces effets secondaires sur les variables by-ref. Ces effets secondaires ne sont que des moyens de renvoyer des entrées supplémentaires (par exemple, mettre à jour un compteur) et pourraient être reformulés en une fonction pure, par exemple en renvoyant un tuple au lieu d'une seule valeur.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.