Perché il lambda di C ++ 11 richiede una parola chiave “mutabile” per l’acquisizione per valore, di default?

Breve esempio:

#include  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 parola chiave mutable ? È abbastanza diverso dal parametro tradizionale che passa alle funzioni con nome. Qual è la logica dietro?

Ho avuto l’impressione che l’intero punto di acquisizione per valore sia quello di consentire all’utente di modificare il temporaneo, altrimenti sono quasi sempre meglio usare l’acquisizione per riferimento, no?

Qualsiasi illuminazione?

(Sto usando MSVC2010 tra l’altro. AFAIK dovrebbe essere standard)

Richiede di essere mutable perché, per impostazione predefinita, un object 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.

Il tuo codice è quasi equivalente a questo:

 #include  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 lambda come a generare una class con operator () che di default è const se non dici che è mutabile.

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

Ho avuto l’impressione che l’intero punto di acquisizione per valore sia quello di consentire all’utente di modificare il temporaneo, altrimenti sono quasi sempre meglio usare l’acquisizione per riferimento, no?

La domanda è, è “quasi”? Un caso d’uso frequente sembra essere quello di restituire o superare lambda:

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

Penso che il mutable non sia un caso di “quasi”. Considero “capture-by-value” come “permettimi di usare il suo valore dopo che l’ quadro catturata muore” piuttosto che “permettimi di cambiarne una copia”. Ma forse questo può essere discusso.

FWIW, Herb Sutter, un noto membro del comitato di standardizzazione C ++, fornisce una risposta diversa a quella domanda in Lambda Correctness e Usability Issues :

Si consideri questo esempio di uomo 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’object 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' 

Sembra che questa funzione sia stata aggiunta per timore che l’utente non si accorga di averne ricevuto una copia, e in particolare che, dal momento che i lambda sono copiabili, potrebbe cambiare una copia di lambda diversa.

Il suo articolo parla del perché questo dovrebbe essere cambiato in C ++ 14. È breve, ben scritto, vale la pena leggerlo se vuoi sapere “cosa c’è nella mente [del membro del comitato]” riguardo a questa particolare caratteristica.

Vedi questa bozza , sotto 5.1.2 [expr.prim.lambda], subclause 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 dal parametro-dichiarazione-clausola lambda-espressione e dal tipo trailingreturn. Questo operatore di chiamata di funzione è dichiarato const (9.3.1) se e solo se la clausola-dichiarazione-clausola lambdaexpression non è seguita da mutabile.

Modifica sul commento di litb: Forse hanno pensato alla cattura per valore in modo che le modifiche esterne alle variabili non si riflettessero all’interno del lambda? I riferimenti funzionano in entrambe le direzioni, quindi questa è la mia spiegazione. Non so se è buono comunque.

Modifica sul commento di kizzx2: La maggior parte delle volte in cui un lambda deve essere usato è un funtore per gli algoritmi. La costanza di default consente di essere utilizzata in un ambiente costante, proprio come le normali funzioni costqualificate possono essere utilizzate lì, ma non quelle non conteggiate con const . Forse hanno solo pensato di renderlo più intuitivo per quei casi, che sanno cosa succede nella loro mente. 🙂

Devi pensare a 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 class senza nome con attributi ( ambiente in cui è stata dichiarata l’espressione Lambda) e la funzione call ::operator() implementata. Quando si acquisisce una variabile utilizzando il valore copy-by-value , il compilatore creerà un nuovo attributo const nel tipo di chiusura, quindi non è ansible cambiarlo all’interno dell’espressione Lambda perché è un attributo di “sola lettura”, questo è il motivo lo chiamano ” chiusura “, perché in qualche modo stai chiudendo la tua espressione Lambda copiando le variabili dall’angolo superiore in ambito Lambda. Quando usi la parola chiave mutable , l’entity framework catturata diventerà un attributo non-const del tuo tipo di chiusura. Questo è ciò che fa sì che i cambiamenti fatti nella variabile mutabile catturata dal valore, non siano propagati allo scope superiore, ma si mantengano all’interno dello stato Lambda. 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.

Ho avuto l’impressione che l’intero punto di acquisizione per valore sia quello di consentire all’utente di modificare il temporaneo, altrimenti sono quasi sempre meglio usare l’acquisizione per riferimento, no?

n non è un temporaneo. n è un membro dell’object lambda-function creato con l’espressione lambda. L’aspettativa di default è che chiamare il tuo lambda non modifichi il suo stato, quindi è const per impedirti di modificare accidentalmente n .

Esiste ora una proposta per alleviare la necessità di mutable nelle dichiarazioni lambda: n3424

Per estendere la risposta di Puppy, le funzioni lambda sono intese come funzioni pure . Ciò significa che ogni chiamata con un set di input univoco restituisce sempre lo stesso risultato. Definiamo l’ input come l’insieme di tutti gli argomenti più tutte le variabili catturate quando viene chiamato il lambda.

Nelle funzioni pure, l’output dipende esclusivamente dall’input e non da uno 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 su variabili catturate è una tensione sul concetto di pura funzione, perché tutto ciò che una pura funzione dovrebbe fare è restituire un output, sebbene il lambda non muti di certo perché la scrittura avviene con variabili esterne. Anche in questo caso un uso corretto implica che se il lambda viene chiamato con lo stesso input di nuovo, l’output sarà lo stesso ogni volta, nonostante questi effetti collaterali sulle variabili by-ref. Tali effetti collaterali sono solo modi per restituire un input aggiuntivo (ad esempio, aggiornare un contatore) e potrebbero essere riformulati in una funzione pura, ad esempio restituendo una tupla anziché un singolo valore.