Funzioni che scrivi dove le usi
Nella pagina precedente hai visto come l'overloading permette a più funzioni di condividere un nome. Ma a volte non vuoi affatto una funzione con un nome: ti serve un minuscolo pezzo di logica una volta sola, proprio dove lo usi, e dargli un nome aggiungerebbe solo confusione. Ecco cos'è una lambda: una funzione anonima che puoi definire inline.
Una lambda ha una forma caratteristica in quattro parti:
[capture](parameters) -> return_type { body }
Le [] sono il segnale che stai guardando una lambda. Il tipo di ritorno è facoltativo: di solito il compilatore lo deduce. Ecco la più semplice possibile:
greet è solo una variabile (il suo tipo non ha un nome scrivibile, quindi la memorizzi con auto) che puoi chiamare con (). Le lambda con parametri funzionano esattamente come le funzioni normali:
Catture: attingere allo scope circostante
La parte che rende le lambda qualcosa di più di semplici funzioni senza nome è la lista di cattura, cioè le []. Permette alla lambda di usare variabili dello scope in cui è stata definita, non solo i propri parametri.
Cattura per valore con [x]: la lambda riceve una propria copia, congelata nel momento in cui viene creata.
Nota che scale(5) ha stampato 50, usando il valore 10 di factor che esisteva quando la lambda è stata creata. La cattura per valore scatta un'istantanea.
Cattura per riferimento con [&x]: la lambda si riferisce alla variabile originale, vede le modifiche successive e può cambiarla.
Puoi anche catturare tutto ciò che la lambda usa con [=] (tutto per valore) o [&] (tutto per riferimento). Sono comodi, ma essere espliciti, con [total] o [&total], documenta esattamente cosa tocca la lambda ed è più facile da ragionare.
La trappola del riferimento pendente
Catturare per riferimento è tanto potente quanto pericoloso. Il riferimento è valido solo finché la variabile originale è viva. Se la lambda vive più a lungo di ciò che ha catturato, ottieni un riferimento pendente e undefined behavior: il programma potrebbe andare in crash, stampare spazzatura o sembrare funzionare per caso.
Questo è l'errore classico: restituire una lambda che cattura per riferimento una variabile locale.
auto makeCounter() {
int count = 0;
return [&count]() { return ++count; }; // BUG: count muore qui
}
// La lambda restituita ora fa riferimento a memoria distrutta.
Quando makeCounter termina, la sua variabile locale count viene distrutta, ma la lambda ne conserva ancora un riferimento. Chiamare la lambda restituita tocca memoria morta. La soluzione è catturare per valore, così la lambda possiede il proprio stato:
Regola pratica: cattura per riferimento solo quando la lambda viene usata subito e in locale (come con gli algoritmi più sotto). Nel momento in cui una lambda viene memorizzata, restituita o eseguita più tardi, preferisci la cattura per valore.
mutable e tipi di ritorno
Hai notato il mutable nell'ultimo esempio? Per impostazione predefinita, una cattura per valore è const dentro la lambda: puoi leggere la copia ma non modificarla. Aggiungere mutable permette alla lambda di modificare le proprie copie tra una chiamata e l'altra.
mutable agisce solo sulla copia privata della lambda: la seen esterna resta intatta, ed è proprio questo il senso della cattura per valore.
Nella maggior parte dei casi il compilatore deduce il tipo di ritorno senza problemi. Devi scriverlo esplicitamente con -> solo quando c'è ambiguità, per esempio in una lambda che potrebbe restituire tipi diversi in rami diversi:
// Senza -> il compilatore non sa scegliere tra int e double
auto half = [](int n) -> double {
if (n % 2 == 0) return n / 2; // int
return n / 2.0; // double
};
Lambda e algoritmi: il vero vantaggio
Le lambda sono state aggiunte a C++ per passare brevi pezzi di logica agli algoritmi della libreria standard. Prima delle lambda dovevi scrivere una funzione separata con un nome o un goffo oggetto funzione, lontano da dove veniva usato. Ora la logica sta proprio nel punto della chiamata.
L'esempio più comune è un ordinamento personalizzato:
Qui le catture danno il meglio di sé, perché la lambda può portarsi dentro un valore con cui filtrare o contare. Questo codice conta quanti numeri superano una soglia scelta dall'utente:
Poiché queste lambda vengono usate subito e non sopravvivono alla funzione che le contiene, anche catturare per riferimento ([&passMark]) sarebbe sicuro qui, ma la cattura per valore è altrettanto chiara e non resta mai pendente.
Prossimo passo: i puntatori
Le lambda hanno sollevato in silenzio una questione più profonda: quando catturi [&x], la lambda si tiene stretta la posizione di x, e quella posizione resta valida solo finché x è viva. Quest'idea, un valore che si riferisce a dove qualcosa vive in memoria e cosa succede quando ciò a cui punta scompare, è esattamente l'argomento della prossima pagina. Affronteremo i puntatori di petto: come ottenere un indirizzo, come seguirlo e come lo stesso problema del riferimento pendente che hai appena visto si ripresenta in tutto C++.
Domande frequenti
Cos'è una lambda in C++?
Una lambda è una funzione anonima che puoi scrivere inline, proprio dove la usi. La sintassi è [captures](parameters){ body }. È perfetta per operazioni brevi e occasionali, come il confronto che passi a std::sort, senza dover dichiarare altrove una funzione separata con un nome.
Qual è la differenza tra catturare per valore e per riferimento in una lambda C++?
[x] cattura una copia di x congelata nel momento in cui la lambda viene creata. [&x] cattura un riferimento alla x originale, quindi la lambda vede le modifiche successive e può cambiarla. Usa [&] solo quando le variabili catturate vivono di sicuro più a lungo della lambda, altrimenti ottieni un riferimento pendente.
Perché la mia lambda C++ dice che non può modificare una variabile catturata?
Le catture per valore sono const dentro la lambda per impostazione predefinita. Aggiungi la parola chiave mutable, [x]() mutable { x++; }, per permettere alla lambda di modificare la propria copia. Nota che così cambia solo la copia della lambda, non la variabile originale all'esterno.