Perché lambda di C ++ 11 richiede la parola chiave "mutable" per acquisizione per valore, per impostazione predefinita?


256

Breve esempio:

#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 domanda: perché abbiamo bisogno della mutableparola chiave? È abbastanza diverso dal parametro tradizionale che passa alle funzioni con nome. Qual è la logica alla base?

Avevo l'impressione che l'intero punto di acquisizione per valore sia consentire all'utente di modificare il temporaneo, altrimenti sto quasi sempre meglio usando l'acquisizione per riferimento, non è vero?

Qualche illuminazione?

(Sto usando MSVC2010 a proposito. AFAIK questo dovrebbe essere standard)


101
Buona domanda; anche se sono contento che qualcosa sia finalmente constdi default!
xtofl

3
Non è una risposta, ma penso che questa sia una cosa sensata: se prendi qualcosa per valore, non dovresti cambiarlo solo per salvarti 1 copia in una variabile locale. Almeno non commetterai l'errore di cambiare n sostituendo = con &.
stefaanv,

8
@xtofl: non sono sicuro che vada bene, quando tutto il resto non è constdi default.
kizzx2

8
@ Tamás Szelei: non iniziare una discussione, ma IMHO il concetto "facile da imparare" non ha posto nel linguaggio C ++, specialmente nei giorni moderni. Comunque: P
kizzx2

3
"l'intero punto di acquisizione per valore è consentire all'utente di modificare il temporaneo" - No, il punto è che la lambda può rimanere valida oltre la durata di qualsiasi variabile acquisita. Se il lambda C ++ avesse solo la cattura per riferimento, sarebbero inutilizzabili in troppi scenari.
Sebastian Redl,

Risposte:


230

Richiede mutableperché, per impostazione predefinita, un oggetto funzione dovrebbe produrre lo stesso risultato ogni volta che viene chiamato. Questa è la differenza tra una funzione orientata agli oggetti e una funzione che utilizza una variabile globale, in modo efficace.


7
Questo è un buon punto. Sono totalmente d'accordo. In C ++ 0x, tuttavia, non vedo come il default aiuti a far valere quanto sopra. Considera che sono all'estremità ricevente della lambda, ad es void f(const std::function<int(int)> g). Come posso garantire che gsia effettivamente referenzialmente trasparente ? gil fornitore potrebbe aver usato mutablecomunque. Quindi non lo saprò. D'altra parte, se il valore predefinito è non- conste le persone devono aggiungere constinvece di mutablefar funzionare gli oggetti, il compilatore può effettivamente applicare la const std::function<int(int)>parte e ora fpuò supporre che lo gsia const, no?
kizzx2,

8
@ kizzx2: in C ++ non viene applicato nulla , solo suggerito. Come al solito, se fai qualcosa di stupido (requisito documentato per la trasparenza referenziale e poi passi la funzione non referenzialmente trasparente), ottieni tutto ciò che ti viene in mente.
Cucciolo

6
Questa risposta mi ha aperto gli occhi. In precedenza, ho pensato che in questo caso lambda muta solo una copia per l'attuale "corsa".
Zsolt Szatmari,

4
@ZsoltSzatmari Il tuo commento mi ha aperto gli occhi! MrGreen Non ho capito il vero significato di questa risposta finché non ho letto il tuo commento.
Jendas,

5
Non sono d'accordo con la premessa di base di questa risposta. C ++ non ha il concetto di "funzioni che dovrebbero sempre restituire lo stesso valore" in qualsiasi altra parte della lingua. Come principio di progettazione, sarei d'accordo che sia un buon modo per scrivere una funzione, ma non credo che trattiene l'acqua, come il motivo per il comportamento standard.
Ionoclast Brigham,

103

Il tuo codice è quasi equivalente a questo:

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

Quindi potresti pensare a lambdas come a generare una classe con operator () che per impostazione predefinita è const a meno che tu non dica che è mutabile.

Puoi anche pensare a tutte le variabili catturate all'interno di [] (esplicitamente o implicitamente) come membri di quella classe: copie degli oggetti per [=] o riferimenti agli oggetti per [&]. Vengono inizializzati quando dichiari il tuo lambda come se ci fosse un costruttore nascosto.


5
Mentre una bella spiegazione di come sarebbe a consto mutablelambda se implementata come tipi definiti dall'utente equivalenti, la domanda è (come nel titolo ed elaborata da OP nei commenti) perché const è l'impostazione predefinita, quindi questo non risponde.
underscore_d,

36

Avevo l'impressione che l'intero punto di acquisizione per valore sia consentire all'utente di modificare il temporaneo, altrimenti sto quasi sempre meglio usando l'acquisizione per riferimento, non è vero?

La domanda è: è "quasi"? Un caso d'uso frequente sembra essere quello di restituire o superare lambdas:

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

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

Penso che mutablenon sia un caso di "quasi". Considero "acquisizione per valore" come "permettimi di usare il suo valore dopo che l'entità catturata muore" piuttosto che "permettimi di cambiarne una copia". Ma forse questo può essere discusso.


2
Buon esempio. Questo è un caso d'uso molto forte per l'uso dell'acquisizione per valore. Ma perché è predefinito const? Quale scopo raggiunge? mutablesembra fuori posto qui, quando nonconst è l'impostazione predefinita in "quasi" (: P) tutto il resto della lingua.
kizzx2

8
@ kizzx2: Vorrei che constfosse l'impostazione predefinita, almeno le persone sarebbero costrette a considerare la correttezza const: /
Matthieu M.

1
@ kizzx2 guardando nei documenti lambda, mi sembra che lo rendano predefinito in constmodo da poterlo chiamare indipendentemente dal fatto che l'oggetto lambda sia const. Ad esempio, potrebbero passarlo a una funzione prendendo a std::function<void()> const&. Per consentire al lambda di modificare le copie acquisite, nei documenti iniziali i membri dei dati della chiusura venivano definiti mutableautomaticamente internamente. Ora devi inserire manualmente mutablel'espressione lambda. Non ho trovato una motivazione dettagliata però.
Johannes Schaub - lett.


5
A questo punto, per me, la "vera" risposta / logica sembra essere "non sono riusciti a aggirare un dettaglio di implementazione": /
kizzx2

32

FWIW, Herb Sutter, noto membro del comitato di standardizzazione C ++, fornisce una risposta diversa a quella domanda in Problemi di correttezza e usabilità di Lambda :

Considera questo esempio di paglia, in cui il programmatore acquisisce una variabile locale in base al valore e tenta di modificare il valore acquisito (che è una variabile membro dell'oggetto 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’

Questa funzione sembra essere stata aggiunta per la preoccupazione che l'utente potrebbe non rendersi conto di averne una copia, e in particolare che, poiché i lambda sono copiabili, potrebbe cambiare una copia di lambda diversa.

Il suo articolo spiega perché questo dovrebbe essere cambiato in C ++ 14. È breve, ben scritto, che vale la pena leggere se si desidera sapere "cosa c'è nella mente di [membri del comitato]" riguardo a questa particolare caratteristica.


16

Devi pensare qual è il tipo di chiusura della tua funzione Lambda. Ogni volta che dichiari un'espressione Lambda, il compilatore crea un tipo di chiusura, che non è altro che una dichiarazione di classe senza nome con attributi ( ambiente in cui è stata dichiarata l'espressione Lambda) e la chiamata di funzione ::operator()implementata. Quando acquisisci una variabile usando il valore di copia per valore , il compilatore creerà un nuovo constattributo nel tipo di chiusura, quindi non puoi cambiarlo all'interno dell'espressione Lambda perché è un attributo di "sola lettura", ecco perché chiamalo " chiusura ", perché in qualche modo stai chiudendo la tua espressione Lambda copiando le variabili dall'ambito superiore all'ambito Lambda.mutable, l'entità acquisita diventerà un non-constattributo del tipo di chiusura. Questo è ciò che fa sì che le modifiche apportate alla variabile mutabile catturate dal valore non vengano propagate all'ambito superiore, ma rimangano all'interno della lambda con stato. Cerca sempre di immaginare il tipo di chiusura risultante della tua espressione Lambda, che mi ha aiutato molto e spero che possa aiutarti anche tu.


14

Vedi questa bozza , in 5.1.2 [expr.prim.lambda], sottoclauso 5:

Il tipo di chiusura per un'espressione lambda ha un operatore di chiamata di funzione inline pubblica (13.5.4) i cui parametri e tipo di ritorno sono descritti rispettivamente dalla clausola di dichiarazione dei parametri dell'espressione lambda e dal tipo di trailingreturn. Questo operatore di chiamata di funzione è dichiarato const (9.3.1) se e solo se la clausola di dichiarazione dei parametri della lambdaexpression non è seguita da mutable.

Modifica sul commento di litb: Forse hanno pensato alla cattura per valore in modo che le modifiche esterne alle variabili non si riflettano all'interno della lambda? I riferimenti funzionano in entrambi i modi, quindi questa è la mia spiegazione. Non so se va bene comunque.

Modifica sul commento di kizzx2: il maggior numero di volte in cui una lambda deve essere utilizzata è una funzione per gli algoritmi. L'impostazione predefinita constconsente di utilizzarlo in un ambiente costante, proprio come le normali constfunzioni qualificate possono essere utilizzate lì, ma non constquelle non qualificate. Forse hanno solo pensato di renderlo più intuitivo per quei casi, che sanno cosa succede nella loro mente. :)


È lo standard, ma perché lo hanno scritto in questo modo?
kizzx2

@ kizzx2: la mia spiegazione è direttamente sotto quella citazione. :) Si collega un po 'a ciò che dice il litb sulla vita degli oggetti catturati, ma va anche un po' oltre.
Xeo

@Xeo: Oh sì, mi sono perso: P È anche un'altra buona spiegazione per un buon uso della cattura per valore . Ma perché dovrebbe essere constdi default? Ho già ricevuto una nuova copia, mi sembra strano non farmi cambiare - soprattutto non è qualcosa di principalmente sbagliato - vogliono solo che lo aggiunga mutable.
kizzx2

Credo che ci sia stato un tentativo di creare una nuova sintassi della dichiarazione di funzione genrale, molto simile a una lambda di nome. Doveva anche risolvere altri problemi rendendo tutto const per impostazione predefinita. Mai completato, ma le idee si sono cancellate sulla definizione lambda.
Bo Persson,

2
@ kizzx2 - Se potessimo ricominciare tutto da capo, probabilmente avremmo varuna parola chiave per consentire il cambiamento e essere costante come predefinito per tutto il resto. Ora non lo facciamo, quindi dobbiamo conviverci. IMO, C ++ 2011 è uscito abbastanza bene, considerando tutto.
Bo Persson,

11

Avevo l'impressione che l'intero punto di acquisizione per valore sia consentire all'utente di modificare il temporaneo, altrimenti sto quasi sempre meglio usando l'acquisizione per riferimento, non è vero?

nnon è un temporaneo. n è un membro dell'oggetto funzione lambda creato con l'espressione lambda. L'aspettativa di default è che chiamare il tuo lambda non modifichi il suo stato, quindi è costante per impedirti di modificare accidentalmente n.


1
L'intero oggetto lambda è temporaneo, anche i suoi membri hanno una durata temporanea.
Ben Voigt,

2
@Ben: IIRC, mi riferivo al problema che quando qualcuno dice "temporaneo", capisco che significa oggetto temporaneo senza nome , che lo stesso lambda è, ma i suoi membri non lo sono. E anche che da "dentro" la lambda, non importa se la stessa lambda è temporanea. Rileggere la domanda anche se sembrerebbe che OP intendesse dire "n dentro la lambda" quando ha detto "temporaneo".
Martin Ba,

6

Devi capire cosa significa catturare! sta catturando non passare argomenti! diamo un'occhiata ad alcuni esempi di codice:

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

}

Come puoi vedere anche se xè stato modificato in 20lambda sta ancora ritornando 10 ( xè ancora 5all'interno della lambda) Cambiare xall'interno della lambda significa cambiare la stessa lambda ad ogni chiamata (la lambda sta mutando ad ogni chiamata). Per imporre la correttezza lo standard ha introdotto la mutableparola chiave. Specificando un lambda come mutabile stai dicendo che ogni chiamata al lambda potrebbe causare un cambiamento nel lambda stesso. Vediamo un altro esempio:

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'esempio sopra mostra che rendendo mutevole la lambda, cambiare xall'interno della lambda "muta" la lambda ad ogni chiamata con un nuovo valore xche non ha nulla a che fare con il valore reale di xnella funzione principale


4

Esiste ora una proposta per alleviare la necessità di mutabledichiarazioni lambda: n3424


Qualche informazione su cosa ne è venuto? Personalmente penso che sia una cattiva idea, dal momento che la nuova "cattura di espressioni arbitrarie" attenua la maggior parte dei punti deboli.
Ben Voigt,

1
@BenVoigt Sì, sembra un cambiamento per il bene del cambiamento.
Miles Rout,

3
@BenVoigt Anche se per essere onesti, mi aspetto che ci siano probabilmente molti sviluppatori C ++ che non sanno che mutableè anche una parola chiave in C ++.
Miles Rout,

1

Per estendere la risposta di Puppy, le funzioni lambda sono intese come funzioni pure . Ciò significa che ogni chiamata a cui viene assegnato un set di input univoco restituisce sempre lo stesso output. Definiamo l' input come l'insieme di tutti gli argomenti più tutte le variabili acquisite quando viene chiamato lambda.

Nelle funzioni pure l'output dipende esclusivamente dall'input e non da un certo stato interno. Pertanto qualsiasi funzione lambda, se pura, non ha bisogno di cambiare il suo stato ed è quindi immutabile.

Quando una lambda cattura per riferimento, scrivere sulle variabili catturate è una tensione sul concetto di funzione pura, perché tutto ciò che una funzione pura dovrebbe fare è restituire un output, sebbene la lambda non muti certamente perché la scrittura avviene su variabili esterne. Anche in questo caso un uso corretto implica che se si chiama nuovamente lambda con lo stesso input, l'output sarà lo stesso ogni volta, nonostante questi effetti collaterali sulle variabili by-ref. Tali effetti collaterali sono solo modi per restituire alcuni input aggiuntivi (ad esempio aggiornare un contatore) e potrebbero essere riformulati in una funzione pura, ad esempio restituendo una tupla anziché un singolo valore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.