Espressioni lambda e std::function in C++

Espressioni lambda e std::function in C++

Le espressioni lambda, introdotte con C++11, permettono di definire funzioni anonime direttamente nel punto in cui servono, senza doverle dichiarare separatamente. Rendono il codice più conciso e leggibile, soprattutto quando si passano piccoli frammenti di comportamento agli algoritmi della libreria standard. Insieme a std::function, che consente di memorizzare e passare oggetti richiamabili in modo uniforme, costituiscono la base della programmazione funzionale in C++.

Anatomia di una lambda

Una lambda si compone di alcune parti: la lista di cattura tra parentesi quadre, l'elenco dei parametri tra parentesi tonde, un eventuale tipo di ritorno e il corpo tra parentesi graffe. Nella sua forma più semplice, una lambda senza cattura è essenzialmente una funzione priva di nome.

#include <iostream>

int main() {
    // Lambda che somma due interi
    auto add = [](int a, int b) {
        return a + b;
    };

    std::cout << add(4, 7) << std::endl;   // Stampa 11
    return 0;
}

La lista di cattura

La caratteristica che distingue una lambda da una semplice funzione è la capacità di catturare variabili dall'ambito circostante. La lista di cattura specifica quali variabili la lambda può utilizzare e in che modo. La cattura per valore copia la variabile all'interno della lambda, mentre la cattura per riferimento consente alla lambda di accedere e modificare la variabile originale.

int factor = 3;
int total = 0;

// Cattura factor per valore e total per riferimento
auto accumulate = [factor, &total](int n) {
    total += n * factor;
};

accumulate(5);   // total diventa 15

Esistono anche forme di cattura predefinita: scrivere il simbolo di uguale nella lista cattura per valore tutte le variabili usate, mentre il simbolo di e commerciale le cattura tutte per riferimento. È buona pratica, tuttavia, elencare esplicitamente le variabili catturate, per rendere chiaro e controllato ciò che la lambda utilizza.

Lambda modificabili

Per impostazione predefinita, una lambda non può modificare le variabili catturate per valore, perché la sua chiamata è trattata come costante. Aggiungendo la parola chiave mutable dopo l'elenco dei parametri si consente alla lambda di alterare la propria copia interna delle variabili catturate per valore, senza però influire sugli originali.

int counter = 0;

// mutable permette di modificare la copia interna di counter
auto generator = [counter]() mutable {
    counter++;
    return counter;
};

// Ogni chiamata incrementa la copia locale, non l'originale

Lambda generiche

A partire da C++14, i parametri di una lambda possono essere dichiarati con auto, rendendola generica: la stessa lambda funziona con tipi diversi, in modo analogo a un template di funzione. Questa flessibilità è particolarmente utile quando si combinano le lambda con gli algoritmi che operano su contenitori eterogenei.

// Lambda generica: funziona con qualsiasi tipo confrontabile
auto maximum = [](auto a, auto b) {
    return (a > b) ? a : b;
};

auto largerInt = maximum(3, 8);
auto largerDouble = maximum(2.5, 1.1);

Memorizzare oggetti richiamabili con std::function

Talvolta occorre memorizzare una funzione, una lambda o un altro oggetto richiamabile in una variabile, in un membro di classe o in un contenitore, per invocarla in un secondo momento. Il tipo std::function, definito nell'intestazione functional, offre un contenitore generico capace di conservare qualsiasi oggetto richiamabile con una data firma. Il suo utilizzo è più flessibile di auto, al costo di un lieve sovraccarico dovuto all'indirezione.

#include <functional>
#include <vector>
#include <iostream>

int main() {
    // Un contenitore di operazioni con la stessa firma
    std::vector<std::function<int(int, int)>> operations;

    operations.push_back([](int a, int b) { return a + b; });
    operations.push_back([](int a, int b) { return a * b; });

    for (const auto& op : operations) {
        std::cout << op(6, 2) << std::endl;
    }
    return 0;
}

Quando preferire ciascuna forma

La regola pratica è usare auto per memorizzare una lambda quando il tipo concreto non deve essere fissato, perché così si evita ogni sovraccarico. Si ricorre invece a std::function quando serve una firma uniforme per conservare oggetti richiamabili di natura diversa, ad esempio in un contenitore o come parametro di un'interfaccia. Le lambda, combinate con gli algoritmi della libreria standard e con std::function, permettono di esprimere in modo elegante comportamenti che un tempo richiedevano funzioni separate o classi apposite, avvicinando il C++ agli idiomi della programmazione funzionale.