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




std::function (6)

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

Alcuni esempi e casi d'uso sarebbero utili.


Cos'è una funzione lambda?

Il concetto C ++ di una funzione lambda 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 che non valgono la pena di nominarli.

In C ++ una funzione lambda è definita in questo modo

[]() { } // 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 del lambda dovrebbe essere disponibile all'interno del corpo della funzione e come. Può essere o:

  1. un valore: [x]
  2. un riferimento [& x]
  3. qualsiasi variabile attualmente nell'ambito di riferimento per riferimento [&]
  4. uguale a 3, ma per valore [=]

Puoi mescolare uno dei precedenti in una lista separata da virgole [x, &y] .

La lista degli argomenti

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

Il corpo della funzione

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

Restituzione del tipo di detrazione

Se un lambda ha solo un'istruzione return, il tipo restituito può essere omesso e ha il tipo implicito di decltype(return_statement) .

Mutevole

Se un lambda è contrassegnato come mutabile (es. []() mutable { } ) è permesso di mutare i valori che sono stati catturati dal valore.

Casi d'uso

La libreria definita dallo standard ISO trae enormi benefici da lambdas e aumenta l'usabilità di molte barre 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 ampliati con varie proposte.

Lambda Initialized Capture

Ora un elemento della lista di cattura può essere inizializzato con = . Ciò consente di rinominare le variabili e catturarle spostandosi. Un esempio tratto 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 estratto da Wikipedia che mostra come acquisire con std::move :

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

Lambda generico

Lambdas ora può essere generico ( auto sarebbe equivalente a T qui se T era un argomento modello di tipo da qualche parte nel campo circostante):

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

Detrazione del tipo di ritorno migliorata

C ++ 14 consente tipi di ritorno dedotti per ogni funzione e non lo limita alle funzioni return expression; del modulo return expression; . Questo è anche esteso a lambda.


Il problema

C ++ include funzioni generiche utili come std::for_each e std::transform , che può essere molto utile. Sfortunatamente possono anche essere piuttosto complicati da usare, in particolare se il functor che si desidera applicare è unico per la particolare funzione.

#include <algorithm>
#include <vector>

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

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

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

In C ++ 03 potresti essere tentato di scrivere qualcosa come il seguente, per mantenere il functor locale:

void func2(std::vector<int>& 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 lambdas che consente di scrivere un functor anonimo e in linea per sostituire struct f . Per piccoli esempi semplici questo può essere più pulito da leggere (mantiene tutto in un unico posto) e potenzialmente più semplice da mantenere, ad esempio nella forma più semplice:

void func3(std::vector<int>& 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 viene dedotto per te il tipo di ritorno della lambda, ad esempio:

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

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

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

Per risolvere questo è permesso specificare esplicitamente un tipo di ritorno per una funzione lambda, usando -> T :

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

"Catturare" le variabili

Finora non abbiamo usato altro che quello che è stato passato al lambda al suo interno, ma possiamo usare anche altre variabili, all'interno del lambda. Se si desidera accedere ad altre variabili, è possibile utilizzare la clausola di cattura (il [] dell'espressione), che finora non è stata utilizzata in questi esempi, ad esempio:

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

Puoi catturare sia con riferimento che con valore, che puoi specificare usando & e = rispettivamente:

  • [&epsilon] cattura per riferimento
  • [&] cattura tutte le variabili utilizzate nella lambda per riferimento
  • [=] acquisisce tutte le variabili utilizzate nel lambda in base al valore
  • [&, epsilon] acquisisce variabili come con [&], ma epsilon in base al valore
  • [=, &epsilon] acquisisce variabili come con [=], ma epsilon per riferimento

L' operator() generato operator() è const per default, con l'implicazione che captures sarà const quando ci si accede per impostazione predefinita. Questo ha l'effetto che ogni chiamata con lo stesso input produce lo stesso risultato, tuttavia è possibile contrassegnare il lambda come mutable per richiedere che l' operator() prodotto non sia const .


Le espressioni lambda vengono in genere utilizzate per incapsulare gli algoritmi in modo che possano essere passati a un'altra funzione. Tuttavia, è possibile eseguire una 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 passaggio. Una volta che il blocco di codice è completamente parametrizzato (come dimostrato dalla rimozione di & ), è possibile spostare il codice su un percorso esterno e renderlo una funzione normale.

Allo stesso modo, puoi 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 metodo di partizionamento della logica del tuo programma , potresti persino trovare utile passare un'espressione lambda come argomento a un'altra espressione lambda ...

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

Le espressioni Lambda consentono anche di creare funzioni nidificate denominate, che possono essere un modo conveniente per evitare la logica duplicata. Anche l'uso di lambda nominati tende ad essere un po 'più facile per gli occhi (rispetto ai lambda in linea anonimi) quando si passa una funzione non banale come parametro a 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 la successiva profilazione rivela un sovraccarico di inizializzazione significativo per l'oggetto funzione, è possibile scegliere di riscriverlo come una funzione normale.



Una funzione lambda è una funzione anonima creata in linea. Può acquisire variabili come alcuni hanno spiegato, (ad esempio http://www.stroustrup.com/C++11FAQ.html#lambda ) ma ci sono alcune limitazioni. Ad esempio, se c'è un'interfaccia di callback come questa,

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

puoi scrivere una funzione sul posto per usarla come quella passata per applicare sotto:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

Ma non puoi farlo:

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

a causa delle limitazioni nello standard C ++ 11. Se si desidera utilizzare le acquisizioni, è necessario fare affidamento sulla libreria e

#include <functional> 

(o qualche altra libreria STL come algoritmo per ottenerlo indirettamente) e quindi lavorare con std :: function invece di passare le normali funzioni come parametri come questo:

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}

risposte

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

A: Sotto il cofano, è l'oggetto di una classe autogenerata con overloading operator () const . Tale oggetto è chiamato chiusura e creato dal compilatore. Questo concetto di "chiusura" è vicino al concetto di bind di C ++ 11. Ma lambda in genere genera un codice migliore. E le chiamate attraverso le chiusure consentono il pieno allineamento.

Q: Quando dovrei usarne uno?

A: Definire "logica semplice e piccola" e chiedere al compilatore di eseguire la generazione dalla domanda precedente. Dai al compilatore alcune espressioni che vuoi essere all'interno dell'operatore (). Tutto il resto del compilatore genererà per te.

D: Quale classe di problemi risolvono che non era possibile prima della loro introduzione?

A: È una sorta di sintassi dello zucchero come gli operatori che sovraccaricano invece delle funzioni per l' aggiunta personalizzata , operazioni subrtact ... Ma salva più righe di codice non necessario per avvolgere 1-3 linee di logica reale in alcune classi, e così via! Alcuni ingegneri pensano che se il numero di linee è minore allora c'è meno possibilità di fare errori (lo penso anche io)

Esempio di utilizzo

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

Extra su lambda, non coperti dalla domanda. Ignora questa sezione se non sei interessato

1. Valori catturati. Cosa puoi catturare

1.1. È possibile fare riferimento a una variabile con durata di archiviazione statica in lambdas. Sono tutti catturati.

1.2. Puoi usare lambda per i valori di cattura "in base al valore". In tal caso, i vars catturati verranno copiati nell'oggetto funzione (chiusura).

[captureVar1,captureVar2](int arg1){}

1.3. Puoi catturare il riferimento. & - in questo contesto significa riferimento, non puntatori.

   [&captureVar1,&captureVar2](int arg1){}

1.4. Esiste la notazione per catturare tutte le vars non statiche in base al valore o per riferimento

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. Esiste la notazione per catturare tutte le variabili non statiche in base al valore o per riferimento e specificare smth. Di Più. Esempi: Cattura tutti i vars non statici per valore, ma per cattura di riferimento Param2

[=,&Param2](int arg1){} 

Cattura tutte le vars non statiche per riferimento, ma per valore cattura Param2

[&,Param2](int arg1){} 

2. Detrazione del tipo di reso

2.1. Il tipo di ritorno Lambda può essere dedotto se lambda è una espressione. Oppure puoi specificarlo esplicitamente.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

Se lambda ha più di una espressione, il tipo di ritorno deve essere specificato tramite il tipo di ritorno finale. Inoltre, la sintassi simile può essere applicata alle funzioni automatiche e alle funzioni membro

3. Valori catturati. Cosa non puoi catturare

3.1. È possibile acquisire solo vars locali, non variabili membro dell'oggetto.

4. ónonversioni

4.1 !! Lambda non è un puntatore a funzione e non è una funzione anonima, ma i lambda senza acquisizione possono essere convertiti implicitamente in un puntatore a funzione.

ps

  1. Ulteriori informazioni sulla grammatica lambda sono disponibili in Working draft for Programming Language C ++ # 337, 2012-01-16, 5.1.2. Lambda Expressions, p.88

  2. In C ++ 14 sono state aggiunte le funzionalità extra denominate "init capture". Permette di eseguire arbitrariamente la dichiarazione dei membri dei dati di chiusura:

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
    




c++-faq