O que é uma expressão lambda no C ++ 11? Quando eu usaria um? Que classe de problemas eles resolvem que não era possível antes da introdução?
Alguns exemplos e casos de uso seriam úteis.
O que é uma expressão lambda no C ++ 11? Quando eu usaria um? Que classe de problemas eles resolvem que não era possível antes da introdução?
Alguns exemplos e casos de uso seriam úteis.
Respostas:
O C ++ inclui funções genéricas úteis como std::for_each
e std::transform
, que podem ser muito úteis. Infelizmente, eles também podem ser bastante complicados de usar, principalmente se o functor que você deseja aplicar for exclusivo para a função específica.
#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 você usa apenas f
uma vez e nesse local específico, parece exagero escrever uma classe inteira apenas para fazer algo trivial e único.
No C ++ 03, você pode tentar escrever algo como o seguinte, para manter o functor local:
void func2(std::vector<int>& v) {
struct {
void operator()(int) {
// do something
}
} f;
std::for_each(v.begin(), v.end(), f);
}
no entanto, isso não é permitido, f
não pode ser passado para uma função de modelo no C ++ 03.
O C ++ 11 introduz lambdas permite que você escreva um functor anônimo in-line para substituir o struct f
. Para pequenos exemplos simples, isso pode ser mais fácil de ler (mantém tudo em um só lugar) e potencialmente mais simples de manter, por exemplo, da forma mais simples:
void func3(std::vector<int>& v) {
std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}
Funções Lambda são apenas açúcar sintático para functores anônimos.
Em casos simples, o tipo de retorno do lambda é deduzido para você, por exemplo:
void func4(std::vector<double>& v) {
std::transform(v.begin(), v.end(), v.begin(),
[](double d) { return d < 0.00001 ? 0 : d; }
);
}
no entanto, quando você começar a escrever lambdas mais complexas, encontrará rapidamente casos em que o tipo de retorno não pode ser deduzido pelo compilador, por exemplo:
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;
}
});
}
Para resolver isso, você pode especificar explicitamente um tipo de retorno para uma função 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;
}
});
}
Até agora, não usamos nada além do que foi passado para o lambda dentro dele, mas também podemos usar outras variáveis dentro do lambda. Se você deseja acessar outras variáveis, pode usar a cláusula capture (a []
da expressão), que até agora não foi utilizada nesses exemplos, por exemplo:
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;
}
});
}
Você pode capturar por referência e valor, que você pode especificar usando &
e =
respectivamente:
[&epsilon]
captura por referência[&]
captura todas as variáveis usadas no lambda por referência[=]
captura todas as variáveis usadas no lambda por valor[&, epsilon]
captura variáveis como com [&], mas epsilon por valor[=, &epsilon]
captura variáveis como com [=], mas epsilon por referênciaO gerado operator()
é const
por padrão, com a implicação de captura const
quando você os acessa por padrão. Isso tem o efeito de que cada chamada com a mesma entrada produziria o mesmo resultado, no entanto, você pode marcar o lambdamutable
para solicitar que o operator()
que é produzido não seja const
.
const
sempre ...
()
- é passado como um lambda de argumento zero, mas como () const
não corresponde ao lambda, ele procura uma conversão de tipo que permita, que inclui conversão implícita -para-função-ponteiro, e chama assim! Sorrateiro!
std::function<double(int, bool)> f = [](int a, bool b) -> double { ... };
Mas, geralmente, nós deixamos o compilador deduzir do tipo: auto f = [](int a, bool b) -> double { ... };
(e não se esqueça de #include <functional>
)
return d < 0.00001 ? 0 : d;
é garantido o retorno duplo, quando um dos operandos é uma constante inteira (é por causa de uma regra implícita de promoção do operador?: Onde o segundo e o terceiro operando são balanceados entre si pela aritmética usual conversões, independentemente da escolha escolhida). Mudar para 0.0 : d
talvez tornasse o exemplo mais fácil de entender.
O conceito C ++ de uma função lambda se origina no cálculo lambda e na programação funcional. Um lambda é uma função sem nome que é útil (na programação real, não na teoria) para trechos curtos de código que são impossíveis de reutilizar e que não valem a pena nomear.
Em C ++, uma função lambda é definida assim
[]() { } // barebone lambda
ou em toda a sua glória
[]() mutable -> T { } // T is the return type, still lacking throw()
[]
é a lista de captura, ()
a lista de argumentos e {}
o corpo da função.
A lista de captura define o que de fora do lambda deve estar disponível dentro do corpo da função e como. Pode ser:
Você pode misturar qualquer um dos itens acima em uma lista separada por vírgulas [x, &y]
.
A lista de argumentos é a mesma que em qualquer outra função C ++.
O código que será executado quando o lambda for realmente chamado.
Se um lambda tiver apenas uma instrução de retorno, o tipo de retorno poderá ser omitido e terá o tipo implícito de decltype(return_statement)
.
Se um lambda é marcado como mutável (por exemplo []() mutable { }
), é permitido alterar os valores que foram capturados por valor.
A biblioteca definida pelo padrão ISO se beneficia muito das lambdas e aumenta a usabilidade em várias barras, pois agora os usuários não precisam desorganizar seu código com pequenos functores em algum escopo acessível.
Em C ++ 14, lambdas foram estendidas por várias propostas.
Um elemento da lista de captura agora pode ser inicializado com =
. Isso permite renomear variáveis e capturar movendo-se. Um exemplo retirado do padrão:
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 um retirado da Wikipedia mostrando como capturar com std::move
:
auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};
Agora, as lambdas podem ser genéricas ( auto
seria equivalente a T
aqui se houvesse
T
um argumento de modelo de tipo em algum lugar do escopo circundante):
auto lambda = [](auto x, auto y) {return x + y;};
O C ++ 14 permite tipos de retorno deduzidos para todas as funções e não o restringe às funções do formulário return expression;
. Isso também se estende às lambdas.
r = &x; r += 2;
mas isso acontece com o valor original de 4. #
As expressões lambda são normalmente usadas para encapsular algoritmos para que possam ser passados para outra função. No entanto, é possível executar um lambda imediatamente após a definição :
[&](){ ...your code... }(); // immediately executed lambda expression
é funcionalmente equivalente a
{ ...your code... } // simple code block
Isso torna as expressões lambda uma ferramenta poderosa para refatorar funções complexas . Você começa envolvendo uma seção de código em uma função lambda, como mostrado acima. O processo de parametrização explícita pode ser realizado gradualmente com testes intermediários após cada etapa. Depois de ter o bloco de código totalmente parametrizado (como demonstrado pela remoção do &
), você pode mover o código para um local externo e torná-lo uma função normal.
Da mesma forma, você pode usar expressões lambda para inicializar variáveis com base no resultado de um algoritmo ...
int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!
Como uma maneira de particionar a lógica do programa , você pode até achar útil passar uma expressão lambda como argumento para outra expressão lambda ...
[&]( std::function<void()> algorithm ) // wrapper section
{
...your wrapper code...
algorithm();
...your wrapper code...
}
([&]() // algorithm section
{
...your algorithm code...
});
As expressões lambda também permitem criar funções aninhadas nomeadas , que podem ser uma maneira conveniente de evitar lógica duplicada. Usar lambdas nomeadas também tende a ser um pouco mais fácil para os olhos (em comparação com lambdas anônimas inline) ao passar uma função não trivial como parâmetro para outra função. Nota: não esqueça o ponto-e-vírgula após a chave de fechamento.
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 a criação de perfil subsequente revelar uma sobrecarga significativa de inicialização para o objeto de função, você poderá reescrever isso como uma função normal.
if
declarações: if ([i]{ for (char j : i) if (!isspace(j)) return false ; return true ; }()) // i is all whitespace
, assumindo que i
é umstd::string
[](){}();
.
(lambda: None)()
sintaxe do Python é muito mais legível.
main() {{{{((([](){{}}())));}}}}
Respostas
P: O que é uma expressão lambda no C ++ 11?
R: Sob o capô, é o objeto de uma classe gerada automaticamente com operador de sobrecarga () const . Esse objeto é chamado de fechamento e criado pelo compilador. Esse conceito de 'fechamento' está próximo do conceito de ligação do C ++ 11. Mas lambdas normalmente geram código melhor. E chamadas através de fechamentos permitem inlining completo.
P: Quando eu usaria um?
R: Para definir "lógica simples e pequena" e solicitar que o compilador execute a geração da pergunta anterior. Você fornece ao compilador algumas expressões que você deseja que esteja dentro do operador (). Todos os outros compiladores de coisas serão gerados para você.
P: Que tipo de problema eles resolvem que não era possível antes da introdução?
R: É algum tipo de açúcar de sintaxe, como sobrecarga de operadores, em vez de funções para operações personalizadas de adição e subrato ... Mas economiza mais linhas de código desnecessário para envolver 1-3 linhas de lógica real em algumas classes e etc.! Alguns engenheiros pensam que, se o número de linhas for menor, haverá menos chance de cometer erros (também acho)
Exemplo de uso
auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);
Extras sobre lambdas, não cobertos por perguntas. Ignore esta seção se você não tiver interesse
1. Valores capturados. O que você pode capturar
1.1 Você pode fazer referência a uma variável com duração de armazenamento estático em lambdas. Todos eles são capturados.
1.2 Você pode usar o lambda para capturar valores "por valor". Nesse caso, os vars capturados serão copiados para o objeto de função (fechamento).
[captureVar1,captureVar2](int arg1){}
1.3 Você pode capturar ser referência. & - neste contexto significa referência, não ponteiros.
[&captureVar1,&captureVar2](int arg1){}
1.4 Existe notação para capturar todos os vars não estáticos por valor ou por referência
[=](int arg1){} // capture all not-static vars by value
[&](int arg1){} // capture all not-static vars by reference
1.5 Existe uma notação para capturar todos os vars não estáticos por valor ou por referência e especificar smth. Mais. Exemplos: capture todos os vars não estáticos por valor, mas por captura de referência Param2
[=,&Param2](int arg1){}
Capture todos os vars não estáticos por referência, mas por captura de valor Param2
[&,Param2](int arg1){}
2. Dedução do tipo de retorno
2.1 O tipo de retorno lambda pode ser deduzido se lambda for uma expressão. Ou você pode especificá-lo explicitamente.
[=](int arg1)->trailing_return_type{return trailing_return_type();}
Se lambda tiver mais de uma expressão, o tipo de retorno deverá ser especificado por meio do tipo de retorno à direita. Além disso, sintaxe semelhante pode ser aplicada a funções automáticas e funções membro
3. Valores capturados. O que você não pode capturar
3.1 Você pode capturar apenas vars locais, não variáveis de membros do objeto.
4. Conversões
4.1 !! O Lambda não é um ponteiro de função e não é uma função anônima, mas os lambdas sem captura podem ser implicitamente convertidos em um ponteiro de função.
ps
Mais informações sobre a gramática lambda podem ser encontradas no rascunho de trabalho da linguagem de programação C ++ # 337, 2012-01-16, 5.1.2. Expressões Lambda, pág.88
No C ++ 14, o recurso extra chamado "captura de inicialização" foi adicionado. Permite executar arbitrariamente a declaração dos membros dos dados de fechamento:
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){}
não parece ser uma sintaxe válida. O formato correto seria[&,Param2](int arg1){}
Uma função lambda é uma função anônima que você cria em linha. Ele pode capturar variáveis como alguns explicaram (por exemplo, http://www.stroustrup.com/C++11FAQ.html#lambda ), mas existem algumas limitações. Por exemplo, se houver uma interface de retorno de chamada como esta,
void apply(void (*f)(int)) {
f(10);
f(20);
f(30);
}
você pode escrever uma função no local para usá-la como a que foi aplicada abaixo:
int col=0;
void output() {
apply([](int data) {
cout << data << ((++col % 10) ? ' ' : '\n');
});
}
Mas você não pode fazer isso:
void output(int n) {
int col=0;
apply([&col,n](int data) {
cout << data << ((++col % 10) ? ' ' : '\n');
});
}
devido a limitações no padrão C ++ 11. Se você quiser usar capturas, precisará confiar na biblioteca e
#include <functional>
(ou alguma outra biblioteca STL como o algoritmo para obtê-lo indiretamente) e, em seguida, trabalhe com a função std :: em vez de passar funções normais como parâmetros como este:
#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
era um modelo que aceitou um functor, ele iria trabalhar
Uma das melhores explicações de lambda expression
é dada pelo autor de C ++ Bjarne Stroustrup em seu livro ***The C++ Programming Language***
capítulo 11 ( ISBN-13: 978-0321563842 ):
What is a lambda expression?
Uma expressão lambda , às vezes também chamada de função lambda ou (estritamente falando incorretamente, mas coloquialmente) como lambda , é uma notação simplificada para definir e usar um objeto de função anônima . Em vez de definir uma classe nomeada com um operador (), criar um objeto dessa classe e, finalmente, invocá-lo, podemos usar uma abreviação.
When would I use one?
Isso é particularmente útil quando queremos passar uma operação como argumento para um algoritmo. No contexto de interfaces gráficas do usuário (e em outros lugares), essas operações são frequentemente chamadas de retornos de chamada .
What class of problem do they solve that wasn't possible prior to their introduction?
Aqui eu acho que todas as ações feitas com a expressão lambda podem ser resolvidas sem elas, mas com muito mais código e complexidade muito maior. Expressão lambda, este é o caminho da otimização para o seu código e uma maneira de torná-lo mais atraente. Tão triste por Stroustup:
maneiras eficazes de otimizar
Some examples
via expressão 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';
});
}
ou via função
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';
}
};
ou mesmo
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 você precisar, pode citar lambda expression
como abaixo:
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);
}
Ou assuma outra amostra simples
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);
}
);
}
irá gerar a próxima
0 0
1
0 0
1
0 0
1
0 0
1
0 0
1
0 ordenadox - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;
[]
- esta é uma lista de captura ou lambda introducer
: se lambdas
não houver acesso ao ambiente local, podemos usá-la.
Citação do livro:
O primeiro caractere de uma expressão lambda é sempre [ . Um introdutor lambda pode assumir várias formas:
• [] : uma lista de capturas vazia. Isso implica que nenhum nome local do contexto circundante pode ser usado no corpo lambda. Para essas expressões lambda, os dados são obtidos de argumentos ou de variáveis não locais.
• [&] : captura implicitamente por referência. Todos os nomes locais podem ser usados. Todas as variáveis locais são acessadas por referência.
• [=] : captura implicitamente por valor. Todos os nomes locais podem ser usados. Todos os nomes se referem a cópias das variáveis locais obtidas no ponto de chamada da expressão lambda.
• [lista de captura]: captura explícita; a lista de captura é a lista de nomes de variáveis locais a serem capturadas (ou seja, armazenadas no objeto) por referência ou por valor. Variáveis com nomes precedidos por & são capturadas por referência. Outras variáveis são capturadas por valor. Uma lista de captura também pode conter esse e nomes seguidos por ... como elementos.
• [&, capture-list] : captura implicitamente por referência todas as variáveis locais com nomes não mencionados na lista. A lista de captura pode conter isso. Os nomes listados não podem ser precedidos por &. Variáveis nomeadas na lista de captura são capturadas por valor.
• [=, capture-list] : captura implicitamente por valor todas as variáveis locais com nomes não mencionados na lista. A lista de captura não pode conter isso. Os nomes listados devem ser precedidos por &. As variáveis nomeadas na lista de captura são capturadas por referência.
Observe que um nome local precedido por & é sempre capturado por referência e um nome local não precedido por & é sempre capturado por valor. Somente a captura por referência permite a modificação de variáveis no ambiente de chamada.
Additional
Lambda expression
formato
Referências adicionais:
for (int x : v) { if (x % m == 0) os << x << '\n';}
Bem, um uso prático que descobri é a redução do código da placa da caldeira. Por exemplo:
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);
//...
}
Sem o lambda, você pode precisar fazer algo para bsize
casos diferentes . Claro que você poderia criar uma função, mas e se você quiser limitar o uso dentro do escopo da função de usuário da alma? a natureza do lambda cumpre esse requisito e eu o uso nesse caso.
Os lambda's em c ++ são tratados como "função disponível on the go". sim, está literalmente em movimento, você define; use-o; e quando o escopo da função pai termina, a função lambda desaparece.
O c ++ o introduziu no c ++ 11 e todos começaram a usá-lo como em todos os lugares possíveis. o exemplo e o que é lambda pode ser encontrado aqui https://en.cppreference.com/w/cpp/language/lambda
descreverei o que não existe, mas é essencial saber para todo programador em c ++
O Lambda não deve ser usado em todos os lugares e todas as funções não podem ser substituídas pelo lambda. Também não é o mais rápido comparar com a função normal. porque tem algumas despesas gerais que precisam ser tratadas pelo lambda.
certamente ajudará a reduzir o número de linhas em alguns casos. ele pode ser basicamente usado para a seção de código, que está sendo chamada na mesma função uma ou mais vezes e esse trecho de código não é necessário em nenhum outro lugar, para que você possa criar uma função autônoma para ele.
Abaixo está o exemplo básico de lambda e o que acontece em segundo plano.
Código de usuário:
int main()
{
// Lambda & auto
int member=10;
auto endGame = [=](int a, int b){ return a+b+member;};
endGame(4,5);
return 0;
}
Como a compilação a expande:
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;
}
como você pode ver, que tipo de sobrecarga ele adiciona quando você o usa. portanto, não é uma boa ideia usá-los em qualquer lugar. pode ser usado em locais onde são aplicáveis.
Um problema que ele resolve: Código mais simples que lambda para uma chamada no construtor que usa uma função de parâmetro de saída para inicializar um membro const
Você pode inicializar um membro const da sua classe, com uma chamada para uma função que define seu valor, devolvendo sua saída como um parâmetro de saída.