Boa tarde amigos Hoje preparamos para você uma tradução da primeira parte do artigo
“Lambdas: from C ++ 11 to C ++ 20” . A publicação deste material está programada para coincidir com o lançamento do curso
"C ++ Developer" , que começa amanhã.
As expressões Lambda são uma das adições mais poderosas do C ++ 11 e continuam a evoluir a cada novo padrão de idioma. Neste artigo, examinaremos sua história e examinaremos a evolução dessa parte importante do C ++ moderno.

A segunda parte está disponível aqui:
Lambdas: do C ++ 11 ao C ++ 20, parte 2EntradaEm uma reunião local do Grupo de Usuários C ++, tivemos uma sessão de programação ao vivo sobre o "histórico" das expressões lambda. A conversa foi liderada pelo especialista em C ++, Tomasz Kamiński (
consulte o perfil do Thomas no LinkedIn ). Aqui está o evento:
Lambdas: de C ++ 11 a C ++ 20 - Grupo de usuários C ++ CracóviaDecidi pegar o código de Thomas (com a permissão dele!), Descrever e criar um artigo separado.
Começaremos explorando o C ++ 03 e a necessidade de expressões funcionais locais compactas. Em seguida, passamos ao C ++ 11 e C ++ 14. Na segunda parte da série, veremos mudanças no C ++ 17 e até daremos uma olhada no que acontecerá no C ++ 20.
Lambdas em C ++ 03Desde o início, os
std::algorithms
do STL, como
std::sort
, podiam pegar qualquer objeto chamado e chamá-lo em elementos de contêiner. No entanto, no C ++ 03, isso envolvia apenas ponteiros para funções e functors.
Por exemplo:
#include <iostream> #include <algorithm> #include <vector> struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
Código em execução:
@WandboxMas o problema era que era necessário escrever uma função ou functor separado em um escopo diferente, e não no escopo da chamada do algoritmo.
Como uma solução em potencial, considere escrever uma classe de functor local - já que o C ++ sempre suporta essa sintaxe. Mas isso não funciona ...
Dê uma olhada neste código:
int main() { struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
Tente compilá-lo com
-std=c++98
e você verá o seguinte erro no GCC:
error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor'
Essencialmente, no C ++ 98/03, você não pode criar uma instância de um modelo com um tipo local.
Devido a todas essas limitações, o Comitê começou a desenvolver um novo recurso que podemos criar e chamar de "no local" ... "expressões lambda"!
Se olharmos para
N3337 - a versão final do C ++ 11, veremos uma seção separada para lambdas:
[expr.prim.lambda] .
Ao lado de C ++ 11Eu acho que as lambdas foram adicionadas sabiamente ao idioma. Eles usam a nova sintaxe, mas o compilador a estende para uma classe real. Portanto, temos todas as vantagens (e algumas vezes desvantagens) de uma linguagem realmente estritamente tipada.
Aqui está um exemplo de código básico que também mostra o objeto functor local correspondente:
#include <iostream> #include <algorithm> #include <vector> int main() { struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), someInstance); std::for_each(v.begin(), v.end(), [] (int x) { std::cout << x << '\n'; } ); }
Exemplo:
@WandBoxVocê também pode conferir o CppInsights, que mostra como o compilador estende o código:
Veja este exemplo:
CppInsighs: teste lambdaNeste exemplo, o compilador converte:
[] (int x) { std::cout << x << '\n'; }
Em algo semelhante a este (formulário simplificado):
struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance;
Sintaxe da expressão lambda:
[] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | | |
Algumas definições antes de começarmos:
De
[expr.prim.lambda # 2] :
A avaliação de uma expressão lambda resulta em um pré-valor temporário. Este objeto temporário é chamado de
objeto de fechamento .
E de
[expr.prim.lambda # 3] :
O tipo de expressão lambda (que também é o tipo de um objeto de fechamento) é um tipo único de não união sem nome da classe chamado
tipo de fechamento .
Alguns exemplos de expressões lambda:
Por exemplo:
[](float f, int a) { return a*f; } [](MyClass t) -> int { auto a = t.compute(); return a; } [](int a, int b) { return a < b; }
Tipo LambdaComo o compilador gera um nome exclusivo para cada lambda, não é possível conhecê-lo com antecedência.
auto myLambda = [](int a) -> double { return 2.0 * a; }
Além disso
[expr.prim.lambda] :
O tipo de fechamento associado à expressão lambda possui um construtor padrão remoto ([dcl.fct.def.delete]) e um operador de atribuição remota.
Portanto, você não pode escrever:
auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy;
Isso resultará no seguinte erro no GCC:
error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor
Operador de chamadaO código que você coloca no corpo lambda é "traduzido" no código do operador () do tipo de fechamento correspondente.
Por padrão, este é um método constante interno. Você pode alterá-lo especificando mutable após declarar os parâmetros:
auto myLambda = [](int a) mutable { std::cout << a; }
Embora o método constante não seja um "problema" para um lambda sem uma lista de capturas vazia ... é importante quando você deseja capturar alguma coisa.
Captura[] não apenas introduz um lambda, mas também contém uma lista de variáveis capturadas. Isso é chamado de lista de captura.
Ao capturar uma variável, você cria um membro de cópia dessa variável no tipo de fechamento. Então, dentro do corpo lambda, você pode acessá-lo.
A sintaxe básica é:
- [&] - captura por referência, todas as variáveis no armazenamento automático são declaradas no escopo
- [=] - captura por valor, o valor é copiado
- [x, & y] - captura explicitamente x por valor e y por referência
Por exemplo:
int x = 1, y = 1; { std::cout << x << " " << y << std::endl; auto foo = [&x, &y]() { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; }
Você pode brincar com o exemplo completo aqui:
@WandboxEmbora especificar
[=]
ou
[&]
possa ser conveniente - uma vez que captura todas as variáveis no armazenamento automático, é mais óbvio capturar variáveis explicitamente. Assim, o compilador pode alertá-lo sobre efeitos indesejados (consulte, por exemplo, notas sobre variáveis globais e estáticas)
Você também pode ler mais no parágrafo 31 do Effective Modern C ++ de Scott Meyers: "Evite os modos de captura padrão".
E uma citação importante:
Os fechamentos em C ++ não aumentam a vida útil dos links capturados.
MutávelPor padrão, o operador do tipo de fechamento () é constante e você não pode modificar as variáveis capturadas dentro do corpo de uma expressão lambda.
Se você deseja alterar esse comportamento, é necessário adicionar a palavra-chave mutável após a lista de parâmetros:
int x = 1, y = 1; std::cout << x << " " << y << std::endl; auto foo = [x, y]() mutable { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl;
No exemplo acima, podemos alterar os valores de x e y ... mas essas são apenas cópias de x e y do escopo anexado.
Captura global de variáveisSe você tiver um valor global e usar [=] em um lambda, poderá pensar que o valor global também é capturado pelo valor ... mas não é.
int global = 10; int main() { std::cout << global << std::endl; auto foo = [=] () mutable { ++global; }; foo(); std::cout << global << std::endl; [] { ++global; } (); std::cout << global << std::endl; [global] { ++global; } (); }
Você pode jogar com o código aqui:
@Wandbox
Somente variáveis no armazenamento automático são capturadas. O GCC pode até emitir o seguinte aviso:
warning: capture of variable 'global' with non-automatic storage duration
Esse aviso aparecerá apenas se você capturar explicitamente a variável global; portanto, se você usar
[=]
, o compilador não o ajudará.
O compilador Clang é mais útil, pois gera um erro:
error: 'global' cannot be captured because it does not have automatic storage duration
Consulte
@WandboxCapturando variáveis estáticasCapturar variáveis estáticas é semelhante a capturar global:
#include <iostream> void bar() { static int static_int = 10; std::cout << static_int << std::endl; auto foo = [=] () mutable { ++static_int; }; foo(); std::cout << static_int << std::endl; [] { ++static_int; } (); std::cout << static_int << std::endl; [static_int] { ++static_int; } (); } int main() { bar(); }
Você pode jogar com o código aqui:
@WandboxConclusão:
10 11 12
E novamente, um aviso aparecerá apenas se você capturar explicitamente uma variável estática; portanto, se você usar
[=]
, o compilador não o ajudará.
Captura de Membro de ClasseVocê sabe o que acontece depois de executar o seguinte código:
#include <iostream> #include <functional> struct Baz { std::function<void()> foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
O código declara um objeto Baz e chama
foo()
. Observe que
foo()
retorna um lambda (armazenado na
std::function
que captura um membro da classe.
Como usamos objetos temporários, não podemos ter certeza do que acontecerá quando f1 e f2 forem chamados. Este é um problema de link pendente que causa comportamento indefinido.
Da mesma forma:
struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo();
Brincar com o código
@WandboxNovamente, se você especificar capturar explicitamente ([s]):
std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; }
O compilador evitará seu erro:
In member function 'std::function<void()> Baz::foo()': error: capture of non-variable 'Baz::s' error: 'this' was not captured for this lambda function ...
Veja um exemplo:
@WandboxObjetos somente capazes de moverSe você tiver um objeto que só pode ser movido (por exemplo, unique_ptr), não poderá colocá-lo em um lambda como uma variável capturada. A captura por valor não funciona, portanto, você pode capturar apenas por referência ... no entanto, isso não será transferido para você e, provavelmente, não é o que você queria.
std::unique_ptr<int> p(new int[10]); auto foo = [p] () {};
Salvando constantesSe você capturar uma variável constante, a constância é preservada:
int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo();
Veja o código:
@WandboxTipo de retornoNo C ++ 11, você pode ignorar
trailing
tipo lambda de retorno e o compilador o produzirá para você.
Inicialmente, a saída do tipo de valor de retorno era limitada a lambdas contendo uma instrução de retorno, mas essa restrição foi rapidamente removida, pois não havia problemas com a implementação de uma versão mais conveniente.
Consulte
Relatórios de defeitos de linguagem principal do C ++ e problemas aceitos (obrigado a Thomas por encontrar o link certo!)
Assim, começando com C ++ 11, o compilador pode inferir o tipo do valor de retorno se todas as instruções de retorno puderem ser convertidas para o mesmo tipo.
Se todas as instruções de retorno retornarem a expressão e os tipos de retorno após a conversão lvalue em rvalue (7.1 [conv.lval]), matriz em ponteiro (7.2 [conv.array]) e função em ponteiro (7.3 [conv. func]) é o mesmo que o tipo genérico;
auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; };
Você pode jogar com o código aqui:
@WandboxExistem duas
return
no lambda acima, mas todas apontam para o
double
, para que o compilador possa inferir o tipo.
IIFE - Expressão de Função Invocada ImediatamenteNos nossos exemplos, defini um lambda e o chamei usando o objeto de fechamento ... mas também pode ser chamado imediatamente:
int x = 1, y = 1; [&]() { ++x; ++y; }();
Essa expressão pode ser útil na inicialização complexa de objetos constantes.
const auto val = []() { }();
Escrevi mais sobre isso no
post IIFE para inicialização complexa .
Converter em um ponteiro de funçãoO tipo de fechamento para uma expressão lambda sem captura tem uma função implícita não virtual aberta de converter uma constante em um ponteiro para uma função que possui o mesmo parâmetro e tipos de retorno que o operador de chamar uma função do tipo de fechamento. O valor retornado por essa função de conversão deve ser o endereço da função, que quando chamada tem o mesmo efeito que chamar o operador de uma função de um tipo semelhante a um tipo de fechamento.
Em outras palavras, você pode converter lambdas sem capturas em um ponteiro de função.
Por exemplo:
#include <iostream> void callWith10(void(* bar)(int)) { bar(10); } int main() { struct { using f_ptr = void(*)(int); void operator()(int s) const { return call(s); } operator f_ptr() const { return &call; } private: static void call(int s) { std::cout << s << std::endl; }; } baz; callWith10(baz); callWith10([](int x) { std::cout << x << std::endl; }); }
Você pode jogar com o código aqui:
@WandboxMelhorias no C ++ 14N4140 standard e lambda:
[expr.prim.lambda] .
O C ++ 14 adicionou duas melhorias significativas nas expressões lambda:
- Capturas com Inicializador
- Lambdas comuns
Esses recursos resolvem vários problemas visíveis no C ++ 11.
Tipo de retornoA saída do tipo de valor de retorno da expressão lambda foi atualizada para obedecer às regras de saída automática para funções.
[expr.prim.lambda # 4]O tipo de retorno do lambda é auto, que é substituído pelo tipo de retorno à direita, se for fornecido e / ou inferido a partir das instruções de retorno, conforme descrito em [dcl.spec.auto].
Capturas com InicializadorEm resumo, podemos criar uma nova variável de membro do tipo de fechamento e usá-la dentro da expressão lambda.
Por exemplo:
int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); }
Isso pode resolver vários problemas, por exemplo, com tipos que estão disponíveis apenas para movimentação.
MovendoAgora podemos mover o objeto para um membro do tipo de fechamento:
#include <memory> int main() { std::unique_ptr<int> p(new int[10]); auto foo = [x=10] () mutable { ++x; }; auto bar = [ptr=std::move(p)] {}; auto baz = [p=std::move(p)] {}; }
OtimizaçãoOutra idéia é usá-lo como uma técnica de otimização potencial. Em vez de calcular algum valor toda vez que chamamos lambda, podemos calculá-lo uma vez no inicializador:
#include <iostream> #include <algorithm> #include <vector> #include <memory> #include <iostream> #include <string> int main() { using namespace std::string_literals; std::vector<std::string> vs; std::find_if(vs.begin(), vs.end(), [](std::string const& s) { return s == "foo"s + "bar"s; }); std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; }); }
Capturar uma variável de membroUm inicializador também pode ser usado para capturar uma variável de membro. Em seguida, podemos obter uma cópia da variável membro e não nos preocupar com links pendentes.
Por exemplo:
struct Baz { auto foo() { return [s=s] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
Você pode jogar com o código aqui:
@Wandbox
Em
foo()
capturamos uma variável de membro copiando-a para o tipo de fechamento. Além disso, usamos auto para gerar todo o método (anteriormente, em C ++ 11, poderíamos usar
std::function
).
Expressões lambda genéricasOutra melhoria significativa é a lambda generalizada.
Começando com C ++ 14, você pode escrever:
auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world");
Isso é equivalente a usar uma declaração de modelo em uma instrução de chamada do tipo de fechamento:
struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance;
Esse lambda generalizado pode ser muito útil quando é difícil inferir um tipo.
Por exemplo:
std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } };
Estou errado aqui? A entrada tem o tipo correto?
.
.
.
Provavelmente não, pois o tipo de valor para std :: map é
std::pair<const Key, T>
. Então, meu código fará cópias adicionais das linhas ...
Isso pode ser corrigido com
auto
:
std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } );
Você pode jogar com o código aqui:
@WandboxConclusãoQue história!
Neste artigo, começamos desde os primeiros dias de expressões lambda no C ++ 03 e C ++ 11 e passamos para uma versão aprimorada no C ++ 14.
Você viu como criar um lambda, qual é a estrutura básica dessa expressão, qual é uma lista de capturas e muito mais.
Na próxima parte do artigo, passaremos para o C ++ 17 e conheceremos os recursos futuros do C ++ 20.
A segunda parte está disponível aqui:
Lambdas: do C ++ 11 ao C ++ 20, parte 2
Referências
C ++ 11 -
[expr.prim.lambda]C ++ 14 -
[expr.prim.lambda]Expressões lambda em C ++ | Documentos da MicrosoftDesmistificando lambdas em C ++ - Sticky Bits - Desenvolvido por Feabhas; Sticky Bits - Desenvolvido por Feabhas
Estamos aguardando seus comentários e convidamos todos os interessados no curso
"Desenvolvedor C ++" .