Lambdas: de C ++ 11 a C ++ 20. Parte 1

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 2

Entrada

Em 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óvia

Decidi 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 ++ 03

Desde 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: @Wandbox

Mas 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 ++ 11

Eu 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: @WandBox

Você também pode conferir o CppInsights, que mostra como o compilador estende o código:

Veja este exemplo:

CppInsighs: teste lambda

Neste 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 Lambda

Como 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 chamada

O 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: @Wandbox

Embora 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ável

Por 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áveis

Se 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 @Wandbox

Capturando variáveis ​​estáticas

Capturar 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: @Wandbox

Conclusã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 Classe

Você 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 @Wandbox

Novamente, 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: @Wandbox

Objetos somente capazes de mover

Se 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 constantes

Se 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: @Wandbox

Tipo de retorno

No 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: @Wandbox

Existem 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 Imediatamente

Nos 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; }(); // <-- call () std::cout << x << " " << y << std::endl; 

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ção
O 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: @Wandbox

Melhorias no C ++ 14

N4140 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 retorno

A 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 Inicializador

Em 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.

Movendo

Agora 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ção

Outra 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 membro

Um 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éricas

Outra 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 } }; //      pair<const string, int>! std::for_each(std::begin(numbers), std::end(numbers), [](const std::pair<std::string, int>& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

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: @Wandbox

Conclusão

Que 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 Microsoft
Desmistificando 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 ++" .

Source: https://habr.com/ru/post/pt444524/


All Articles