Che 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.
Che 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.
Risposte:
C ++ include utili funzioni generiche come std::for_each
e std::transform
, che possono essere molto utili. Purtroppo possono anche essere abbastanza ingombrante per l'uso, in particolare se il funtore si desidera applicare è unica 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 solo f
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 potresti essere tentato di scrivere qualcosa di simile al seguente, per mantenere il funzione locale:
void func2(std::vector<int>& v) {
struct {
void operator()(int) {
// do something
}
} f;
std::for_each(v.begin(), v.end(), f);
}
tuttavia ciò non è consentito, f
non può essere passato a una funzione modello in C ++ 03.
C ++ 11 introduce lambdas che consente di scrivere un funzione anonimo in linea per sostituire il struct f
. Per piccoli semplici esempi 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 professionisti anonimi.
In casi semplici viene dedotto il tipo di ritorno della lambda, ad es .:
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) { return d < 0.00001 ? 0 : d; }
);
}
tuttavia, quando si inizia a scrivere lambda più complessi, si verificheranno 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, ti è 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;
}
});
}
Finora non abbiamo usato altro che ciò che è stato passato al lambda al suo interno, ma possiamo anche usare altre variabili, all'interno del lambda. Se si desidera accedere ad altre variabili, è possibile utilizzare la clausola di acquisizione ( []
l'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 acquisire sia per riferimento che per valore, che puoi specificare usando &
e =
rispettivamente:
[&epsilon]
cattura per riferimento[&]
cattura tutte le variabili utilizzate nella lambda per riferimento[=]
cattura tutte le variabili utilizzate in lambda per valore[&, epsilon]
cattura variabili come con [&], ma epsilon per valore[=, &epsilon]
cattura variabili come con [=], ma epsilon per riferimentoIl generato operator()
è const
per impostazione predefinita, con l'implicazione che saranno le acquisizioni const
quando si accede a loro per impostazione predefinita. Ciò ha l'effetto che ogni chiamata con lo stesso input produrrebbe lo stesso risultato, tuttavia è possibile contrassegnare la lambda in modomutable
da richiedere che operator()
ciò che viene prodotto non lo sia const
.
const
sempre ...
()
- viene passato come lambda a argomento zero, ma poiché () const
non corrisponde al lambda, cerca una conversione di tipo che lo consenta, che include il cast implicito -to-funzione-puntatore e quindi lo chiama! Subdolo!
std::function<double(int, bool)> f = [](int a, bool b) -> double { ... };
Ma di solito, lasciamo che il compilatore deduca il tipo: auto f = [](int a, bool b) -> double { ... };
(e non dimenticare di #include <functional>
)
return d < 0.00001 ? 0 : d;
è garantito il ritorno doppio, quando uno degli operandi è una costante intera (è a causa di una regola di promozione implicita dell'operatore?: Dove il 2o e 3o operando sono bilanciati l'uno contro l'altro attraverso la solita aritmetica conversioni indipendentemente da quale viene scelto). Cambiare in 0.0 : d
forse renderebbe l'esempio più facile da capire.
Il concetto C ++ di una funzione lambda ha origine nel calcolo lambda e nella programmazione funzionale. Una lambda è una funzione senza nome che è utile (nella programmazione attuale, non in teoria) per brevi frammenti di codice che sono impossibili da riutilizzare e non meritano di essere nominati.
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()
[]
è l'elenco di acquisizione, ()
l'elenco degli argomenti e {}
il corpo della funzione.
L'elenco di acquisizione definisce ciò che dall'esterno della lambda dovrebbe essere disponibile all'interno del corpo della funzione e come. Può essere:
È possibile combinare uno dei precedenti in un elenco separato da virgole [x, &y]
.
L'elenco degli argomenti è lo stesso di qualsiasi altra funzione C ++.
Il codice che verrà eseguito quando viene effettivamente chiamato lambda.
Se un lambda ha solo un'istruzione return, il tipo restituito può essere omesso e ha il tipo implicito di decltype(return_statement)
.
Se un lambda è contrassegnato come mutabile (ad es. []() mutable { }
) È consentito mutare i valori che sono stati catturati dal valore.
La libreria definita dallo standard ISO beneficia fortemente di lambdas e aumenta l'usabilità di parecchi bar poiché ora gli utenti non devono ingombrare il loro codice con piccoli funzioni in un ambito accessibile.
In C ++ 14 le lambda sono state estese da varie proposte.
Ora è possibile inizializzare un elemento dell'elenco di acquisizione =
. Ciò consente di rinominare le variabili e di acquisirle spostandole. 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<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};
Lambdas ora può essere generico ( auto
sarebbe equivalente a T
qui se
T
fosse un argomento tipo template da qualche parte nell'ambito circostante):
auto lambda = [](auto x, auto y) {return x + y;};
C ++ 14 consente tipi di restituzione dedotti per ogni funzione e non lo limita alle funzioni del modulo return expression;
. Questo si estende anche agli lambda.
r = &x; r += 2;
ma ciò accade al valore originale di 4.
Le espressioni lambda vengono in genere utilizzate per incapsulare algoritmi in modo che possano essere passate a 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
Ciò 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 in una posizione esterna e renderlo una normale funzione.
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 modo di partizionare la logica del 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 inoltre di creare funzioni nidificate denominate , che può essere un modo conveniente per evitare la logica duplicata. L'uso del nome lambdas tende anche ad essere un po 'più semplice per gli occhi (rispetto agli 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 profilazione successiva rivela un notevole sovraccarico di inizializzazione per l'oggetto funzione, è possibile scegliere di riscriverlo come una normale funzione.
if
dichiarazioni :, if ([i]{ for (char j : i) if (!isspace(j)) return false ; return true ; }()) // i is all whitespace
supponendo che i
sia unstd::string
[](){}();
.
(lambda: None)()
sintassi di Python è molto più leggibile.
main() {{{{((([](){{}}())));}}}}
risposte
D: Cos'è un'espressione lambda in C ++ 11?
A: Sotto il cofano, è l'oggetto di una classe autogenerata con operatore di sovraccarico () const . Tale oggetto è chiamato chiusura e creato dal compilatore. Questo concetto di "chiusura" è vicino al concetto di associazione di C ++ 11. Ma i lambda in genere generano un codice migliore. E le chiamate attraverso le chiusure consentono un allineamento completo.
Q: Quando dovrei usarne uno?
A: Definire "logica semplice e piccola" e chiedere al compilatore di eseguire la generazione dalla domanda precedente. Dai a un compilatore alcune espressioni che vuoi essere all'interno dell'operatore (). Tutti gli altri compilatori di roba genereranno per te.
D: Quale classe di problemi risolvono che non era possibile prima della loro introduzione?
A: È una specie di zucchero di sintassi come il sovraccarico degli operatori invece delle funzioni per operazioni personalizzate di aggiunta, subrtact ... Ma salva più righe di codice non necessario per avvolgere 1-3 righe di logica reale in alcune classi, ecc.! Alcuni ingegneri pensano che se il numero di linee è inferiore, allora ci sono meno possibilità di commettere 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 ti interessa
1. Valori acquisiti. Cosa puoi catturare
1.1. È possibile fare riferimento a una variabile con durata di memorizzazione statica in lambdas. Sono tutti catturati.
1.2. È possibile utilizzare lambda per acquisire i valori "per valore". In tal caso i vars catturati verranno copiati nell'oggetto funzione (chiusura).
[captureVar1,captureVar2](int arg1){}
1.3. È possibile acquisire come riferimento. & - in questo contesto significano riferimento, non puntatori.
[&captureVar1,&captureVar2](int arg1){}
1.4. Esiste una notazione per acquisire tutte le variabili non statiche per 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 una notazione per acquisire tutte le variabili non statiche per valore o per riferimento e specificare smth. Di Più. Esempi: cattura tutte le variabili non statiche per valore, ma per riferimento cattura Param2
[=,&Param2](int arg1){}
Cattura tutte le variabili non statiche per riferimento, ma per valore cattura Param2
[&,Param2](int arg1){}
2. Detrazione del tipo restituito
2.1. Il tipo di ritorno lambda può essere dedotto se lambda è un'espressione. Oppure puoi specificarlo esplicitamente.
[=](int arg1)->trailing_return_type{return trailing_return_type();}
Se lambda ha più di un'espressione, è necessario specificare il tipo restituito tramite il tipo restituito finale. Inoltre, sintassi simile può essere applicata alle funzioni automatiche e alle funzioni membro
3. Valori acquisiti. Cosa non puoi catturare
3.1. È possibile acquisire solo variabili locali, non variabili membro dell'oggetto.
4. Сonversions
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
Ulteriori informazioni sulla grammatica lambda sono disponibili nella bozza di lavoro per il linguaggio di programmazione C ++ # 337, 2012-01-16, 5.1.2. Espressioni lambda, p.88
In C ++ 14 sono state aggiunte le funzionalità extra che sono state denominate "init capture". Permette di eseguire una dichiarazione arbitraria 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);};
[&,=Param2](int arg1){}
non sembra essere una sintassi valida. La forma corretta sarebbe[&,Param2](int arg1){}
Una funzione lambda è una funzione anonima che si crea in linea. Può catturare variabili come alcuni hanno spiegato (ad esempio http://www.stroustrup.com/C++11FAQ.html#lambda ) ma ci sono alcune limitazioni. Ad esempio, se esiste 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 da applicare di seguito:
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 di limitazioni nello standard C ++ 11. Se si desidera utilizzare acquisizioni, è necessario fare affidamento sulla libreria e
#include <functional>
(o qualche altra libreria STL come l'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');
});
}
apply
fosse un modello che ha accettato un funzione, funzionerebbe
Una delle migliori spiegazioni di lambda expression
è data dall'autore del C ++ Bjarne Stroustrup nel suo libro ***The C++ Programming Language***
capitolo 11 ( ISBN-13: 978-0321563842 ):
What is a lambda expression?
Un'espressione lambda , talvolta indicato anche come una lambda funzione o (in senso stretto correttamente, ma colloquialmente) come lambda , è una notazione semplificata per definire e utilizzare un oggetto funzione anonima . Invece di definire una classe nominata con un operatore (), in seguito creando un oggetto di quella classe e infine invocandolo, possiamo usare una scorciatoia.
When would I use one?
Ciò è particolarmente utile quando vogliamo passare un'operazione come argomento a un algoritmo. Nel contesto delle interfacce utente grafiche (e altrove), tali operazioni sono spesso chiamate callback .
What class of problem do they solve that wasn't possible prior to their introduction?
Qui immagino che ogni azione fatta con lambda possa essere risolta senza di loro, ma con molto più codice e una complessità molto maggiore. Espressione lambda: questo è il modo di ottimizzare il tuo codice e un modo per renderlo più attraente. Come triste da Stroustup:
modi efficaci di ottimizzazione
Some examples
tramite espressione lambda
void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
for_each(begin(v),end(v),
[&os,m](int x) {
if (x%m==0) os << x << '\n';
});
}
o tramite la funzione
class Modulo_print {
ostream& os; // members to hold the capture list int m;
public:
Modulo_print(ostream& s, int mm) :os(s), m(mm) {}
void operator()(int x) const
{
if (x%m==0) os << x << '\n';
}
};
o anche
void print_modulo(const vector<int>& v, ostream& os, int m)
// output v[i] to os if v[i]%m==0
{
class Modulo_print {
ostream& os; // members to hold the capture list
int m;
public:
Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
void operator()(int x) const
{
if (x%m==0) os << x << '\n';
}
};
for_each(begin(v),end(v),Modulo_print{os,m});
}
se ti serve puoi nominare lambda expression
come di seguito:
void print_modulo(const vector<int>& v, ostream& os, int m)
// output v[i] to os if v[i]%m==0
{
auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
for_each(begin(v),end(v),Modulo_print);
}
O supponiamo un altro semplice esempio
void TestFunctions::simpleLambda() {
bool sensitive = true;
std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});
sort(v.begin(),v.end(),
[sensitive](int x, int y) {
printf("\n%i\n", x < y);
return sensitive ? x < y : abs(x) < abs(y);
});
printf("sorted");
for_each(v.begin(), v.end(),
[](int x) {
printf("x - %i;", x);
}
);
}
genererà il prossimo
0
1
0
1
0
1
0
1
0
1
0 ordinatix - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;
[]
- questo è un elenco di acquisizione o lambda introducer
: se lambdas
non è necessario l'accesso al proprio ambiente locale, è possibile utilizzarlo.
Citazione dal libro:
Il primo carattere di un'espressione lambda è sempre [ . Un introduttore lambda può assumere varie forme:
• [] : un elenco di acquisizione vuoto. Ciò implica che nessun nome locale dal contesto circostante può essere usato nel corpo lambda. Per tali espressioni lambda, i dati sono ottenuti da argomenti o da variabili non locali.
• [&] : cattura implicitamente per riferimento. Tutti i nomi locali possono essere utilizzati. Tutte le variabili locali sono accessibili per riferimento.
• [=] : cattura implicitamente per valore. Tutti i nomi locali possono essere utilizzati. Tutti i nomi si riferiscono a copie delle variabili locali prese nel punto di richiamo dell'espressione lambda.
• [elenco di acquisizione]: acquisizione esplicita; l'elenco di acquisizione è l'elenco di nomi di variabili locali da acquisire (ovvero, memorizzati nell'oggetto) per riferimento o per valore. Le variabili con nomi precedute da & sono acquisite per riferimento. Altre variabili vengono acquisite per valore. Un elenco di acquisizione può contenere anche questo e nomi seguiti da ... come elementi.
• [&, elenco di acquisizione] : acquisisce implicitamente per riferimento tutte le variabili locali con nomi non menzionati nell'elenco. L'elenco di acquisizione può contenere questo. I nomi elencati non possono essere preceduti da &. Le variabili nominate nell'elenco di acquisizione vengono acquisite per valore.
• [=, elenco di acquisizione] : acquisisce implicitamente per valore tutte le variabili locali con nomi non menzionati nell'elenco. L'elenco di acquisizione non può contenere questo. I nomi elencati devono essere preceduti da &. Le variabili indicate nell'elenco di acquisizione vengono acquisite per riferimento.
Si noti che un nome locale preceduto da & viene sempre catturato per riferimento e un nome locale non preceduto da & viene sempre catturato dal valore. Solo l'acquisizione per riferimento consente la modifica delle variabili nell'ambiente chiamante.
Additional
Lambda expression
formato
Riferimenti aggiuntivi:
for (int x : v) { if (x % m == 0) os << x << '\n';}
Bene, un uso pratico che ho scoperto è la riduzione del codice della piastra della caldaia. Per esempio:
void process_z_vec(vector<int>& vec)
{
auto print_2d = [](const vector<int>& board, int bsize)
{
for(int i = 0; i<bsize; i++)
{
for(int j=0; j<bsize; j++)
{
cout << board[bsize*i+j] << " ";
}
cout << "\n";
}
};
// Do sth with the vec.
print_2d(vec,x_size);
// Do sth else with the vec.
print_2d(vec,y_size);
//...
}
Senza lambda, potresti dover fare qualcosa per bsize
casi diversi . Ovviamente potresti creare una funzione ma cosa succede se vuoi limitare l'utilizzo nell'ambito della funzione utente soul? la natura di lambda soddisfa questo requisito e lo uso per quel caso.
Le lambda in c ++ sono trattate come "on the go available available". sì, è letteralmente in movimento, lo definisci; usalo; e al termine dell'ambito della funzione genitore, la funzione lambda scompare.
c ++ lo ha introdotto in c ++ 11 e tutti hanno iniziato a usarlo come in ogni luogo possibile. l'esempio e cos'è lambda è disponibile qui https://en.cppreference.com/w/cpp/language/lambda
descriverò ciò che non c'è ma essenziale da sapere per ogni programmatore c ++
Lambda non è pensato per essere usato ovunque e ogni funzione non può essere sostituita con lambda. Inoltre, non è il più veloce rispetto alla normale funzione. perché ha delle spese generali che devono essere gestite da lambda.
sicuramente aiuterà a ridurre il numero di linee in alcuni casi. può essere sostanzialmente utilizzato per la sezione di codice, che viene chiamata nella stessa funzione una o più volte e quel pezzo di codice non è necessario in nessun altro posto in modo da poter creare una funzione autonoma per esso.
Di seguito è riportato l'esempio di base di lambda e cosa succede in background.
Codice utente:
int main()
{
// Lambda & auto
int member=10;
auto endGame = [=](int a, int b){ return a+b+member;};
endGame(4,5);
return 0;
}
Come la compilazione lo espande:
int main()
{
int member = 10;
class __lambda_6_18
{
int member;
public:
inline /*constexpr */ int operator()(int a, int b) const
{
return a + b + member;
}
public: __lambda_6_18(int _member)
: member{_member}
{}
};
__lambda_6_18 endGame = __lambda_6_18{member};
endGame.operator()(4, 5);
return 0;
}
così come puoi vedere, che tipo di overhead aggiunge quando lo usi. quindi non è una buona idea usarli ovunque. può essere utilizzato nei luoghi in cui sono applicabili.
Un problema che risolve: codice più semplice di lambda per una chiamata nel costruttore che utilizza una funzione di parametro di output per inizializzare un membro const
Puoi inizializzare un membro const della tua classe, con una chiamata a una funzione che imposta il suo valore restituendo l'output come parametro di output.