Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial
  • Lezione 75 di 93
  • livello avanzato
Indice lezioni

Espressioni lambda

Link copiato negli appunti

A partire dallo standard C++11, la specifica del linguaggio prevede la possibilità di definire funzioni anonime mediante l'uso di espressioni lambda.

Queste ultime sono un costrutto mutuato da linguaggi e paradigmi di programmazione preesistenti per motivi di convenienza più che di necessità. In questo senso si può dire che l'introduzione delle espressioni lambda ha avuto impatto più sul piano idiomatico che su quello semantico.

In effetti, l'uso di questo costrutto non incrementa la capacità espressiva del linguaggio di per sé, ma lo arricchisce di una sintassi che consente di implementare chiusure e callback in modo molto meno prolisso.

Sintassi di base

Gli elementi di base per la definizione di una espressione lambda sono illustrati nel diagramma seguente:

  • Clausola di cattura: contiene una lista di variabili catturate per valore o riferimento dal contesto di definizione della espressione lambda. Questa parte del costrutto si presta a definire in modo molto snello una chiusura funzionale, cioè l'associazione tra una sequenza di istruzioni ed un insieme di variabili appartenenti ad un contesto esterno.
  • Lista di parametri: è un elemento opzionale del costrutto, impiegata tipicamente quando una espressione lambda è usata come callback, ad esempio nell'invocazione di uno degli algoritmi della libreria standard.
  • Corpo di istruzioni: la sequenza di istruzioni che manipolano i dati catturati dal contesto esterno o passati per argomento.

Per completezza, anticipiamo che la definizione di una espressione lambda può essere arricchita con ulteriori elementi sintattici e che essi saranno discussi nel dettaglio nel seguito.

Come anticipato manca un identificatore, cioè un nome, perché sostanzialmente possiamo guardare alle espressione lambda alla stregua di elementi usa e getta del nostro programma.

In pratica una espressione lambda è un altro modo di definire in C++ un entità invocabile che può essere usata in maniera diretta o passata come argomento ad un'altra funzione, ma la cui applicazione è di scarsa utilità al di fuori del contesto di definizione. Per questi casi d'uso, la definizione di una espressione lambda risulta meno prolissa della definizione di una funzione o un funtore ad hoc ed ha il grande vantaggio di essere localizzata nel contesto d'uso.

Il listato seguente mostra una espressione lambda usata come callback per la funzione std::transform, usata per applicare un fattore di scala agli elementi di un vettore di numeri interi:

#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
    int num = 3;
    std::vector<int> v = {1, 2, 3, 4};
    std::transform(v.begin(), v.end(), v.begin(), [num] (const int& e) { return e * num; });
    for (auto e : v)
        std::cout << e << "\n";
    return 0;
}

Si osservi che in questo esempio il fattore di scala num viene catturato per valore dal contesto esterno.

La possibilità di catturare variabili dall'esterno è ciò che effettivamente differenzia l'uso di espressioni lambda dai tradizionali puntatori a funzione. In questa modalità d'impiego, definire una espressione lambda equivale a definire una classe dotata di un sovraccarico dell'operatore di chiamata a funzione e di dati membro in cui vengono copiati i valori o gli indirizzi delle variabili catturate, solo che è il compilatore che fa la maggior parte del lavoro per noi.

Cattura per valore o riferimento

La clausola di cattura può essere specializzata per istruire il compilatore sul modo corretto di effettuare l'associazione con le variabili esterne.

Le parentesi quadre possono includere una lista di identificatori preceduti separati da virgole per indicare cattura per valore. Il simbolo = è usato per indicare che la espressione cattura il contesto esterno nella sua interezza per valore.

In alternativa è possibile specificare la cattura per riferimento apponendo il simbolo & come prefisso di uno o più identificatori oppure a solo per indicare che la cattura per riferimento deve essere estesa a tutte le variabili del contesto esterno che non sono esplicitamente catturate per valore.

Proprio come avviene nel caso del passaggio di parametri ad una funzione, catturare una variabile per riferimento implica che è possibile alterare il suo valore all'interno della espressione lambda.

Catturare per riferimento consente inoltre di evitare di copiare i valori catturati, tuttavia nel caso di esecuzione asincrona espone a potenziali violazioni di accesso, qualora il contesto di cattura e quello di esecuzione della espressione lambda siano disgiunti.

Di seguito sono forniti alcuni esempi di cattura:

// cattura tutto il contesto esterno per valore ad eccezione di var
[=, &var]
// cattura tutto il contesto esterno per riferimento ad eccezione di var
[&, var]
// cattura var1 per riferimento e var2 per valore
[&var1, var2]
// cattura il contesto esterno per valore
[=]
// cattura il contesto esterno per riferimento
[&]
// nessuna cattura
[]

Valore di ritorno

Il meccanismo di inferenza dei tipi è sufficiente nella maggior parte dei casi per la corretta definizione del tipo di ritorno di una espressione lambda.

Tuttavia, in caso di ambiguità o quando il corpo della funzione anonima contempla molteplici percorsi d'uscita, potrebbe essere necessario definire il tipo del valore restituito in modo esplicito mediante l'uso di un altro costrutto introdotto con lo standard C++11, detto trailing return type, come mostrato nel diagramma seguente:

Diversamente dal caso della definizione della firma di una funzione in cui il tipo di ritorno può essere apposto in testa o in coda, per le espressioni lambda l'uso del costrutto trailing return type è l'unica sintassi ammessa per la definizione del tipo restituito.

Come esempio, nel listato precedente è possibile aggiungere la definizione esplicita del tipo di ritorno della espressione lambda nel modo seguente:

[num] (const int& e) -> int { return e * num; });

Ti consigliamo anche