Cos'è un'espressione lambda in C++11?

Cos'è un'espressione lambda in C++11? Quando dovrei usarne una? Quale classe di problemi risolvono che non era possibile prima della loro introduzione?

Alcuni esempi e casi d'uso sarebbero utili.

Soluzione

Il problema

Il C++ include utili funzioni generiche come std::for_each e std::transform, che possono essere molto utili. Sfortunatamente possono anche essere piuttosto ingombranti da usare, in particolare se il functor che vorreste applicare è unico per quella particolare funzione.

#include 
#include 

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

Se usate f solo una volta e in quel posto specifico, sembra eccessivo scrivere un'intera classe solo per fare qualcosa di banale e una tantum.

In C++03 si potrebbe essere tentati di scrivere qualcosa come il seguente, per mantenere il funtore locale:

void func2(std::vector& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

tuttavia questo non è permesso, f non può essere passato ad una funzione template in C++03.

La nuova soluzione

C++11 introduce i lambda che permettono di scrivere un funtore anonimo in linea per sostituire la struct f. Per piccoli esempi semplici questo può essere più pulito da leggere (mantiene tutto in un posto) e potenzialmente più semplice da mantenere, per esempio nella forma più semplice:

void func3(std::vector& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Le funzioni lambda sono solo zucchero sintattico per i funtori anonimi.

Tipi di ritorno

In casi semplici il tipo di ritorno della lambda viene dedotto per voi, ad es:

void func4(std::vector& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

tuttavia quando iniziate a scrivere lambda più complessi incontrerete rapidamente casi in cui il tipo di ritorno non può essere dedotto dal compilatore, ad es:

void func4(std::vector& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Per risolvere questo problema si può specificare esplicitamente un tipo di ritorno per una funzione lambda, usando -> T:

void func4(std::vector& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

"Cattura" variabili

Finora non abbiamo usato nient'altro che ciò che è stato passato alla lambda al suo interno, ma possiamo usare anche altre variabili, all'interno della lambda. Se volete accedere ad altre variabili potete usare la clausola di cattura (il [] dell'espressione), che finora è stata inutilizzata in questi esempi, ad es:

void func5(std::vector& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

Potete catturare sia per riferimento che per valore, che potete specificare usando rispettivamente & e =:

  • [&epsilon] cattura per riferimento
  • [&] cattura tutte le variabili usate nella lambda per riferimento
  • [=] cattura tutte le variabili usate nella lambda per valore
  • [&, epsilon] cattura le variabili come con [&], ma epsilon per valore
  • [=, &epsilon] cattura le variabili come con [=], ma epsilon per riferimento

L'operatore() generato è const per default, con l'implicazione che le catture saranno const quando vi si accede per default. Questo ha l'effetto che ogni chiamata con lo stesso input produrrebbe lo stesso risultato, tuttavia è possibile marcare la lambda come mutable per richiedere che l' operatore() che viene prodotto non sia const.

Commentari (14)

Cos'è una funzione lambda?

Il concetto di funzione lambda in C++ ha origine nel calcolo lambda e nella programmazione funzionale. Un lambda è una funzione senza nome che è utile (nella programmazione reale, non nella teoria) per brevi frammenti di codice che sono impossibili da riutilizzare e non vale la pena nominare.

In C++ una funzione lambda è definita così

[]() { } // barebone lambda

o in tutta la sua gloria

[]() mutable -> T { } // T is the return type, still lacking throw()

[] è la lista di cattura, () la lista degli argomenti e {} il corpo della funzione.

La lista di cattura

La lista di cattura definisce cosa dall'esterno della lambda dovrebbe essere disponibile all'interno del corpo della funzione e come. Può essere sia:

  1. un valore: [x]
  2. un riferimento [&x]
  3. qualsiasi variabile attualmente in ambito per riferimento [&]
  4. come il 3, ma per valore [=]

Potete mescolare qualsiasi di questi elementi in una lista separata da virgole [x, &y].

L'elenco degli argomenti

L'elenco degli argomenti è lo stesso di qualsiasi altra funzione C++.

Il corpo della funzione

Il codice che verrà eseguito quando il lambda verrà effettivamente chiamato.

Deduzione del tipo di ritorno

Se una lambda ha solo una dichiarazione di ritorno, il tipo di ritorno può essere omesso e ha il tipo implicito di decltype(return_statement).

Mutable

Se una lambda è marcata come mutabile (es. []() mutable { }) le è permesso di mutare i valori che sono stati catturati da value.

Casi d'uso

La libreria definita dallo standard ISO beneficia pesantemente delle lambda e innalza l'usabilità di diversi livelli poiché ora gli utenti non devono ingombrare il loro codice con piccoli funtori in qualche ambito accessibile.

C++14

In C++14 i lambda sono stati estesi da varie proposte.

Catture lambda inizializzate

Un elemento della lista di cattura può ora essere inizializzato con =. Questo permette di rinominare le variabili e di catturare spostandosi. Un esempio preso dallo standard:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

e uno preso da Wikipedia che mostra come catturare con std::move:

auto ptr = std::make_unique(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

Lambda generici

Le lambda possono ora essere generiche (auto sarebbe equivalente a T qui se T fosse un argomento template di tipo da qualche parte nell'ambito circostante):

auto lambda = [](auto x, auto y) {return x + y;};

Deduzione del tipo di ritorno migliorata

C++14 permette di dedurre i tipi di ritorno per ogni funzione e non lo limita alle funzioni della forma return expression;. Questo è anche esteso ai lambda.

Commentari (2)

Le espressioni lambda sono tipicamente usate per incapsulare algoritmi in modo che possano essere passati ad un'altra funzione. Tuttavia, è possibile eseguire un lambda immediatamente dopo la definizione:

[&](){ ...your code... }(); // immediately executed lambda expression

è funzionalmente equivalente a

{ ...your code... } // simple code block

Questo rende le espressioni lambda un potente strumento per il refactoring di funzioni complesse. Si inizia avvolgendo una sezione di codice in una funzione lambda come mostrato sopra. Il processo di parametrizzazione esplicita può quindi essere eseguito gradualmente con test intermedi dopo ogni passo. Una volta che avete il blocco di codice completamente parametrizzato (come dimostrato dalla rimozione del &), potete spostare il codice in una posizione esterna e renderlo una normale funzione.

Allo stesso modo, potete usare le espressioni lambda per inizializzare le variabili in base al risultato di un algoritmo...

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

Come un modo per partizionare la logica del vostro programma, potreste anche trovare utile passare un'espressione lambda come argomento ad un'altra espressione lambda...

[&]( std::function algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

Le espressioni lambda vi permettono anche di creare funzioni annidate, che può essere un modo conveniente per evitare la logica duplicata. L'uso di lambda con nome tende anche ad essere un po' più facile per gli occhi (rispetto ai lambda anonimi in linea) quando si passa una funzione non banale come parametro ad un'altra funzione. *Nota: non dimenticare il punto e virgola dopo la parentesi graffa di chiusura.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

Se il profiling successivo rivela un significativo overhead di inizializzazione per l'oggetto funzione, potreste scegliere di riscriverlo come una normale funzione.

Commentari (18)