Warum benötigt das Lambda von C ++ 11 standardmäßig das Schlüsselwort "veränderbar" für die Erfassung nach Wert?

256

Kurzes Beispiel:

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

Die Frage: Warum brauchen wir das mutableSchlüsselwort? Es unterscheidet sich erheblich von der herkömmlichen Parameterübergabe an benannte Funktionen. Was ist der Grund dafür?

Ich hatte den Eindruck, dass der Sinn der Erfassung nach Wert darin besteht, dem Benutzer zu ermöglichen, die temporäre Erfassung zu ändern - ansonsten ist es fast immer besser, die Erfassung nach Referenz zu verwenden, nicht wahr?

Irgendwelche Erleuchtungen?

(Ich benutze übrigens MSVC2010. AFAIK das sollte Standard sein)

kizzx2
quelle
101
Gute Frage; obwohl ich froh bin, dass endlich etwas conststandardmäßig ist!
xtofl
3
Keine Antwort, aber ich denke, das ist eine vernünftige Sache: Wenn Sie etwas nach Wert nehmen, sollten Sie es nicht ändern, nur um 1 Kopie in einer lokalen Variablen zu speichern. Zumindest machen Sie nicht den Fehler, n zu ändern, indem Sie = durch & ersetzen.
Stefaanv
8
@xtofl: Ich bin mir nicht sicher, ob es gut ist, wenn alles andere nicht conststandardmäßig ist.
kizzx2
8
@ Tamás Szelei: Nicht um ein Argument zu beginnen, aber meiner Meinung nach hat das Konzept "leicht zu lernen" keinen Platz in der C ++ - Sprache, besonders in der heutigen Zeit. Wie auch immer: P
kizzx2
3
"Der springende Punkt bei der Erfassung nach Wert ist, dass der Benutzer das temporäre Element ändern kann." - Nein, der springende Punkt ist, dass das Lambda möglicherweise über die Lebensdauer aller erfassten Variablen hinaus gültig bleibt. Wenn C ++ - Lambdas nur Capture-by-Ref hätten, wären sie in viel zu vielen Szenarien unbrauchbar.
Sebastian Redl

Antworten:

230

mutableDies ist erforderlich, da ein Funktionsobjekt standardmäßig bei jedem Aufruf dasselbe Ergebnis liefern sollte. Dies ist der Unterschied zwischen einer objektorientierten Funktion und einer Funktion, die effektiv eine globale Variable verwendet.

Hündchen
quelle
7
Das ist ein guter Punkt. Ich bin vollkommen einverstanden. In C ++ 0x sehe ich jedoch nicht ganz, wie die Standardeinstellung dazu beiträgt, das oben Gesagte durchzusetzen. Betrachten Sie, ich bin am empfangenden Ende des Lambda, zB bin ich void f(const std::function<int(int)> g). Wie kann ich garantieren, dass dies gtatsächlich referenziell transparent ist ? gDer Lieferant könnte mutablesowieso verwendet haben. Also werde ich es nicht wissen. Auf der anderen Seite, wenn der Standard - nicht ist const, und die Menschen müssen hinzufügen conststatt mutableauf Funktionsobjekte, kann der Compiler tatsächlich die Durchsetzung const std::function<int(int)>Teil und jetzt fdavon ausgehen kann , dass gist const, nicht wahr?
kizzx2
8
@ kizzx2: In C ++ wird nichts erzwungen , nur vorgeschlagen. Wie üblich erhalten Sie alles, was Ihnen einfällt, wenn Sie etwas Dummes tun (dokumentierte Anforderung an referenzielle Transparenz und dann nicht referenziell transparente Funktion übergeben).
Welpe
6
Diese Antwort öffnete meine Augen. Zuvor dachte ich, dass Lambda in diesem Fall nur eine Kopie für den aktuellen "Lauf" mutiert.
Zsolt Szatmari
4
@ZsoltSzatmari Dein Kommentar hat mir die Augen geöffnet! : -DIch habe die wahre Bedeutung dieser Antwort erst verstanden, als ich Ihren Kommentar gelesen habe.
Jendas
5
Ich bin mit der Grundvoraussetzung dieser Antwort nicht einverstanden. C ++ hat kein Konzept für "Funktionen sollten immer den gleichen Wert zurückgeben" an einer anderen Stelle in der Sprache. Als Konstruktionsprinzip, würde ich zustimmen , dass es ein guter Weg ist , eine Funktion zu schreiben, aber ich glaube nicht , dass Wasser als hält den Grund für das Standardverhalten.
Ionoclast Brigham
103

Ihr Code entspricht fast dem folgenden:

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

Sie können sich Lambdas also als Generierung einer Klasse mit operator () vorstellen, die standardmäßig const ist, es sei denn, Sie sagen, dass sie veränderbar ist.

Sie können sich auch alle in [] erfassten Variablen (explizit oder implizit) als Mitglieder dieser Klasse vorstellen: Kopien der Objekte für [=] oder Verweise auf die Objekte für [&]. Sie werden initialisiert, wenn Sie Ihr Lambda deklarieren, als ob es einen versteckten Konstruktor gäbe.

Daniel Munoz
quelle
5
Während eine nette Erklärung, wie ein constoder mutableLambda aussehen würde, wenn es als äquivalente benutzerdefinierte Typen implementiert würde, ist die Frage (wie im Titel und von OP in Kommentaren ausgearbeitet), warum dies const die Standardeinstellung ist, sodass dies nicht beantwortet wird.
underscore_d
36

Ich hatte den Eindruck, dass der Sinn der Erfassung nach Wert darin besteht, dem Benutzer zu ermöglichen, die temporäre Erfassung zu ändern - ansonsten ist es fast immer besser, die Erfassung nach Referenz zu verwenden, nicht wahr?

Die Frage ist, ist es "fast"? Ein häufiger Anwendungsfall scheint darin zu bestehen, Lambdas zurückzugeben oder zu übergeben:

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

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

Ich denke, das mutableist kein Fall von "fast". Ich betrachte "Capture-by-Value" als "Erlaube mir, seinen Wert zu verwenden, nachdem die erfasste Entität gestorben ist" anstatt "Erlaube mir, eine Kopie davon zu ändern". Aber vielleicht kann dies argumentiert werden.

Johannes Schaub - litb
quelle
2
Gutes Beispiel. Dies ist ein sehr starker Anwendungsfall für die Verwendung der Werterfassung. Aber warum ist es standardmäßig so const? Welchen Zweck erreicht es? mutablehier scheint fehl am Platz, wenn constist nicht in „fast“ die Standardeinstellung (: P) alles andere in der Sprache.
kizzx2
8
@ kizzx2: Ich wünschte, constes wäre die Standardeinstellung, zumindest wären die Leute gezwungen, die Konstanz zu berücksichtigen: /
Matthieu M.
1
@ kizzx2 Wenn ich in die Lambda-Papiere schaue, scheint es mir, dass sie es als Standard festlegen, constdamit sie es aufrufen können, ob das Lambda-Objekt const ist oder nicht. Zum Beispiel könnten sie es an eine Funktion übergeben, die a übernimmt std::function<void()> const&. Damit das Lambda seine erfassten Kopien ändern kann, wurden in den ersten Unterlagen die Datenelemente des Verschlusses mutableintern automatisch definiert . Jetzt müssen Sie mutableden Lambda-Ausdruck manuell eingeben. Ich habe jedoch keine detaillierte Begründung gefunden.
Johannes Schaub - litb
5
An diesem Punkt scheint mir die "echte" Antwort / Begründung zu sein "sie haben es nicht geschafft, ein Implementierungsdetail zu
umgehen
32

FWIW, Herb Sutter, ein bekanntes Mitglied des C ++ - Standardisierungsausschusses, gibt eine andere Antwort auf diese Frage in Lambda Correctness and Usability Issues :

Betrachten Sie dieses Strohmann-Beispiel, bei dem der Programmierer eine lokale Variable nach Wert erfasst und versucht, den erfassten Wert (der eine Mitgliedsvariable des Lambda-Objekts ist) zu ändern:

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’

Diese Funktion wurde anscheinend aus dem Grund hinzugefügt, dass der Benutzer möglicherweise nicht bemerkt, dass er eine Kopie erhalten hat, und insbesondere, dass Lambdas kopierbar sind, da er möglicherweise eine andere Lambda-Kopie ändert.

In seinem Artikel geht es darum, warum dies in C ++ 14 geändert werden sollte. Es ist kurz, gut geschrieben und lesenswert, wenn Sie wissen möchten, was [Ausschussmitglied] in Bezug auf diese spezielle Funktion denkt.

akim
quelle
16

Sie müssen sich überlegen, welchen Verschlusstyp Ihre Lambda-Funktion hat. Jedes Mal, wenn Sie einen Lambda-Ausdruck deklarieren, erstellt der Compiler einen Schließungstyp, der nichts weniger als eine unbenannte Klassendeklaration mit Attributen ( Umgebung, in der der Lambda-Ausdruck deklariert wurde) und dem ::operator()implementierten Funktionsaufruf ist . Wenn Sie eine Variable mit copy-by-value erfassen , erstellt der Compiler ein neues constAttribut im Closure-Typ, sodass Sie es im Lambda-Ausdruck nicht ändern können, da es sich um ein schreibgeschütztes Attribut handelt Nennen Sie es einen " Abschluss ", weil Sie Ihren Lambda-Ausdruck auf irgendeine Weise schließen, indem Sie die Variablen aus dem oberen Bereich in den Lambda-Bereich kopieren.mutablewird die erfasste Entität zu einem non-constAttribut Ihres Schließungstyps. Dies führt dazu, dass die Änderungen, die an der durch den Wert erfassten veränderlichen Variablen vorgenommen werden, nicht in den oberen Bereich übertragen werden, sondern im zustandsbehafteten Lambda verbleiben. Versuchen Sie immer, sich den resultierenden Verschlusstyp Ihres Lambda-Ausdrucks vorzustellen, der mir sehr geholfen hat, und ich hoffe, er kann Ihnen auch helfen.

Tarantel
quelle
14

Siehe diesen Entwurf unter 5.1.2 [expr.prim.lambda], Unterabschnitt 5:

Der Schließungstyp für einen Lambda-Ausdruck verfügt über einen öffentlichen Inline-Funktionsaufrufoperator (13.5.4), dessen Parameter und Rückgabetyp durch die Parameterdeklarationsklausel und den Trailingreturn-Typ des Lambda-Ausdrucks beschrieben werden. Dieser Funktionsaufrufoperator wird genau dann als const (9.3.1) deklariert, wenn auf die Parameterdeklarationsklausel des Lambda-Ausdrucks keine veränderbare Klausel folgt.

Bearbeiten Sie den Kommentar von litb: Vielleicht haben sie daran gedacht, nach Wert zu erfassen, damit äußere Änderungen an den Variablen nicht im Lambda widergespiegelt werden? Referenzen funktionieren in beide Richtungen, das ist meine Erklärung. Ich weiß aber nicht, ob es etwas Gutes ist.

Bearbeiten Sie den Kommentar von kizzx2: Die meiste Zeit, wenn ein Lambda verwendet werden soll, ist als Funktor für Algorithmen. Die Standardeinstellung constermöglicht die Verwendung in einer konstanten Umgebung, genau wie dort normal constqualifizierte Funktionen verwendet werden können, nicht constqualifizierte jedoch nicht. Vielleicht haben sie nur daran gedacht, es für jene Fälle intuitiver zu machen, die wissen, was in ihrem Kopf vorgeht. :) :)

Xeo
quelle
Es ist der Standard, aber warum haben sie es so geschrieben?
kizzx2
@ kizzx2: Meine Erklärung steht direkt unter diesem Zitat. :) Es bezieht sich ein wenig auf das, was litb über die Lebensdauer der erfassten Objekte sagt, geht aber auch ein wenig weiter.
Xeo
@Xeo: Oh ja, das habe ich verpasst: P Es ist auch eine weitere gute Erklärung für eine gute Verwendung von Capture-by-Value . Aber warum sollte es conststandardmäßig sein? Ich habe bereits ein neues Exemplar erhalten, es scheint seltsam, mich es nicht ändern zu lassen - insbesondere ist es nicht grundsätzlich falsch - sie möchten nur, dass ich es hinzufüge mutable.
kizzx2
Ich glaube, es wurde versucht, eine neue Syntax für die Deklaration von Genralfunktionen zu erstellen, die einem benannten Lambda sehr ähnlich sieht. Es sollte auch andere Probleme beheben, indem standardmäßig alles const gemacht wird. Nie abgeschlossen, aber die Ideen haben sich auf die Lambda-Definition ausgewirkt.
Bo Persson
2
@ kizzx2 - Wenn wir noch einmal von vorne anfangen könnten, hätten wir wahrscheinlich varein Schlüsselwort, um Änderungen und Konstanten als Standard für alles andere zuzulassen. Jetzt tun wir es nicht, also müssen wir damit leben. IMO, C ++ 2011 kam in Anbetracht aller Aspekte ziemlich gut heraus.
Bo Persson
11

Ich hatte den Eindruck, dass der Sinn der Erfassung nach Wert darin besteht, dem Benutzer zu ermöglichen, die temporäre Erfassung zu ändern - ansonsten ist es fast immer besser, die Erfassung nach Referenz zu verwenden, nicht wahr?

nist keine vorübergehende. n ist ein Mitglied des Lambda-Funktionsobjekts, das Sie mit dem Lambda-Ausdruck erstellen. Die Standarderwartung ist, dass das Aufrufen Ihres Lambda seinen Status nicht ändert. Daher soll verhindert werden, dass Sie versehentlich Änderungen vornehmen n.

Martin Ba
quelle
1
Das gesamte Lambda-Objekt ist vorübergehend, seine Mitglieder haben auch eine vorübergehende Lebensdauer.
Ben Voigt
2
@ Ben: IIRC, ich bezog mich auf das Problem, dass wenn jemand "temporär" sagt, ich es als unbenanntes temporäres Objekt verstehe , das das Lambda selbst ist, aber seine Mitglieder nicht. Und auch, dass es von "innerhalb" des Lambda nicht wirklich wichtig ist, ob das Lambda selbst vorübergehend ist. Erneutes Lesen der Frage, obwohl es den Anschein hat, dass OP nur das "n im Lambda" sagen wollte, als er "vorübergehend" sagte.
Martin Ba
6

Sie müssen verstehen, was Capture bedeutet! Es erfasst nicht das Weitergeben von Argumenten! Schauen wir uns einige Codebeispiele an:

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

}

Wie Sie sehen können x, gibt 20das Lambda immer noch 10 zurück ( xbefindet sich immer noch 5im Lambda). Das Ändern xim Lambda bedeutet, dass das Lambda selbst bei jedem Anruf geändert wird (das Lambda mutiert bei jedem Anruf). Um die Korrektheit zu erzwingen, führte der Standard das mutableSchlüsselwort ein. Wenn Sie ein Lambda als veränderlich angeben, sagen Sie, dass jeder Aufruf des Lambda eine Änderung des Lambda selbst verursachen kann. Sehen wir uns ein anderes Beispiel an:

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

}

Das obige Beispiel zeigt, dass, indem das Lambda veränderbar gemacht wird, das Ändern xinnerhalb des Lambda das Lambda bei jedem Aufruf mit einem neuen Wert "mutiert", der xnichts mit dem tatsächlichen Wert von xin der Hauptfunktion zu tun hat

Soulimane Mammar
quelle
4

Es gibt jetzt einen Vorschlag, um die Notwendigkeit von mutableLambda-Erklärungen zu verringern : n3424

usta
quelle
Irgendwelche Informationen darüber, was daraus wurde? Ich persönlich halte das für eine schlechte Idee, da die neue "Erfassung beliebiger Ausdrücke" die meisten Schmerzpunkte glättet.
Ben Voigt
1
@ BenVoigt Ja, es scheint eine Veränderung zu sein, um der Veränderung willen.
Miles Rout
3
@BenVoigt Um fair zu sein, gehe ich davon aus, dass es wahrscheinlich viele C ++ - Entwickler gibt, die nicht wissen, dass dies mutablesogar ein Schlüsselwort in C ++ ist.
Miles Rout
1

Um die Antwort von Puppy zu erweitern, sollen Lambda-Funktionen reine Funktionen sein . Das bedeutet, dass jeder Aufruf mit einem eindeutigen Eingabesatz immer dieselbe Ausgabe zurückgibt. Definieren wir die Eingabe als die Menge aller Argumente plus aller erfassten Variablen, wenn das Lambda aufgerufen wird.

Bei reinen Funktionen hängt die Ausgabe ausschließlich von der Eingabe und nicht von einem internen Zustand ab. Daher muss jede Lambda-Funktion, wenn sie rein ist, ihren Zustand nicht ändern und ist daher unveränderlich.

Wenn ein Lambda als Referenz erfasst, ist das Schreiben auf erfasste Variablen eine Belastung für das Konzept der reinen Funktion, da eine reine Funktion lediglich eine Ausgabe zurückgeben sollte, obwohl das Lambda nicht unbedingt mutiert, da das Schreiben auf externe Variablen erfolgt. Selbst in diesem Fall impliziert eine korrekte Verwendung, dass, wenn das Lambda erneut mit derselben Eingabe aufgerufen wird, die Ausgabe trotz dieser Nebenwirkungen auf By-Ref-Variablen jedes Mal dieselbe ist. Solche Nebenwirkungen sind nur Möglichkeiten, zusätzliche Eingaben zurückzugeben (z. B. einen Zähler zu aktualisieren) und können in eine reine Funktion umformuliert werden, z. B. die Rückgabe eines Tupels anstelle eines einzelnen Werts.

Attersson
quelle