Expressões lambda

Expressões Lambda

Introdução

A linguagem C++, desde a versão 11, introduziu a possibilidade de utilização expressões lambdas com escopo definido pelo usuário.

Expressões lambdas são extremamente úteis para definir outras funções pequenas passadas como callbacks inline[1].

Expressões lambdas aceitam parâmetros passados por valor ou por referência, valor de retorno, acessam variáveis dentro do escopo, podem ser modeladas com templates, por valor ou por referência. Se quiser assistir uma bela introdução sobre o tema de Stephan Lavajev, um dos principais desenvolvedores da biblioteca Visual Studio C++ 'functional - What's new, and Proper Usage' feita na CPP Con2015

Outro vídeo bem interessante sobre o tema

Back to Basics: Lambdas from Scratch - Arthur O'Dwyer - CppCon 2019
Note

"A título de curiosidade o termo lambda e closure foram emprestados de um sistema computacional conceitual denominado Lambda Calculus, desenvolvido por Alonzo Church em 1930. [2]

"Lambda Calculus pode ser compreendido como o programa mais simples e a menor linguagem de programação possível. Ele consiste de duas partes: um esquema simples função de definição e uma simples regra de transformação. Apenas esses dois componentes foram suficientes para a criação de um modelo genérico formal para descrição de uma linguagem de programação como LISP, Haskell, Clojure e até C++!."[2:1]

A execução de uma expressão lambda cria um objeto closure temporário.

Tip

Se alguma vez você se deparou com a palavra closure e tentou descobrir o significado da palavra e acabou clicando no primeiro link que lhe conduziu a algum artigo do wikipedia e se deparou com palavras como escopo léxico ou função de primeira ordem e logo pensou: Será que eu pesquisei a palavra correta ? Acalme-se! Vamos tentar definir as coisas. Em poucas palavras, um closure é, essencialmente, uma função interna que possui acesso as variáveis locais da função mais externa. Escopo léxico é exatamente isso, ele nos permite acessar o contexto de variáveis de um contexto externo no escopo mais interno [3]. Se uma variável x for declarada dentro de uma função então o escopo léxico desta variável x é o corpo da função onde ela foi declarada. Quais são as regras que definem uma closure?

  1. É necessário ser uma função
  2. Referencia alguma variável ...
  3. ...que é declarado no escopo mais externo

Sintaxe da função lambda


Fonte: https://en.cppreference.com/w/cpp/language/lambda

Exemplo 01 - função lambda

Cada número com sua explicação:

  1. [=, &a, &b\] Introdutor lambda (lambda introducer) ou bloco de captura (capture block)

    1. O que vem dentro dos colchetes são as variáveis que serão capturadas dentro do escopo.
  2. (auto x, auto y) Parâmetros. Na versão foi permitido a atribuição de parâmetros automáticos como x e y neste exemplo.

  3. mutable será explicado mais a frente na seção mutables

  4. int Tipo de retorno, opcional

  5. {return x+ y } Corpo (Body)

Uma expressão lambda começa com um colchetes [ ] que é denominado de introdutor lambda, seguido por chaves { }. O compilador automaticamente transforma uma expressão lambda em uma função-objeto (lambda closure). Ou seja, uma função lambda é apenas um açúcar sintático para funções objetos.

Exemplo 02 - Antes do C++11

Antes da versão C++11, era assim que se construía uma expressão lambda, utilizando functors

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

struct imprime
{
	void operator() (int x) const
	{
		cout << x;
	}
};

std::vector<int> v{1,2,3,4,5,7,8,9,10};

int main()
{
	std::for_each(v.begin(), v.end(), imprime() );
	
}
Saída em Console

1234578910

Exemplo 03 - Depois da versão C++11

A partir da versão C++11, utilizando lambda expression

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

std::vector<int> v{1,2,3,4,5,7,8,9,10};

int main()
{
	std::for_each(v.begin(), v.end(), [](int x) {
		cout << x;
		});

}
Saída em Console

1234578910

Exemplo 04 - Função Lambda gerada pelo compilador

O compilador transforma a função lambda acima neste código:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

struct funcao_lambda_anonima
{
	void operator() (int x) const
	{
		cout << x;
	}
};

std::vector<int> v{1,2,3,4,5,7,8,9,10};

int main()
{
	std::for_each(v.begin(), v.end(), [](int x) {
		cout << x;
		});

}


Saída em Console

1234578910

Função-objeto ou functors

Uma classe que faz overload do operator() é chamada de função-objeto (function object) ou simplesmente functor. [4]

Funções lambda, portanto, são funções-objeto anônimas (ou seja, essas functors que acabamos de definir).

Devido a complexidade das funções objetos, dê preferência na utilização de expressões lambdas.

Uma das vantagens de se usar uma função dentro de um objeto é que ela também possibilita o armazenamento de informação como informações de estado (statefull e stateless).

Stateless é quando você opera com recursos isolados, nenhuma informação sobre transações antigas são armazenadas, e cada uma delas é feita do zero.

Statefull são aqueles que podem ser usados mais de uma vez, com base no contexto das transações anteriores. Dependendo do que aconteceu nelas, isso pode afetar as transações atuais. [5]

Warning

  1. Toda expressão lambda só produz um tipo único e anônimo
  2. Não existe overhead, nem type-erasure
  3. Todas as funções geradas são inline. Confira os exemplos!
  4. Não é aceito overload de expressões lambdas. O compilador gera um erro. É preciso explicitamente dizer qual função você esta querendo invocar. Use static_cast<tipo>(predicado)
  5. Closures produzidas por expressões lambdas são 'cidadãos de primeira classe'. Leia o conceito no Wikipedia - Cidadão de primeira classe
  6. Expressões lambdas podem ser do tipo statefull ou stateless

Captura de variáveis no escopo externo

Na chamada por referência as alterações realizadas nos parâmetros serão refletidas na variável assinalada na chamada da função.

Na chamada por valor, uma cópia da variável será realizada e mudanças na variável não refletirão mudanças na variável de escopo.

Exemplo de capturas[6]

[ ] { } captura nada
[=] { } captura tudo por valor (default)
[&] { } captura tudo por refrência
[x] { } captura x por valor
[&x] { } captura x por referência
[&, x ] { } captura 'x' por valor e todo o resto por referência
[ =, &x] { } captura 'x' por referência e todo o resto por valor
[] (int) {} recebe um inteiro e deduz o tipo de retorno (por valor)
[] () int& {} recebe nada e retorna uma referência para um inteiro
[] (float) mutable char {} recebe um parâmetro float, retorna um char e tem um operador non-const.

As variáveis capturadas serão armazenadas dentro do closure daquela instância lambda.

A versão abre possibilidade para inicializar os valores capturados no escopo com uma expressão arbitrária.

[z=1] {}
[z = std::move(foo)] {}
[x{0}, y{10}] {}

Capturar unique_ptr pelo std::move

auto  up = std::make_unique<foo>()
auto lambda = [ip = std::move(up)]
{
		// ....
}

Exemplo 05 - Usando this

Se a variável this aparece na captura, então o ponteiro é copiado e não existe necessidade de usar this para acessar qualquer variável membro.

#include <iostream>
using namespace std;

class A {
	public:
		int _valor{ 1 }; // Variável membro
		void funcao_membro()
		{
			auto funcao_lambda = [this]() mutable -> void
			{ 
			// Repare que _valor pode ser acessado 
			// sem this, o this fica implícito.
				_valor++;
			};
			funcao_lambda();
		}

};

int main()
{
	setlocale(LC_ALL, "");
	A a;
	cout << "Valor da variável membro _valor antes: " << a._valor << endl;
	a.funcao_membro();
	cout << "Valor da variável membro _valor depois: " << a._valor << endl;
}
Saída em Console

Valor da variável membro _valor antes: 1
Valor da variável membro _valor depois: 2

Exemplo 06 - Usando *this

Na versão incluiu a possibilidade de passar *this que resultará que a instância inteira será copiada em um novo objeto, não somente o ponteiro [2:2]. Isso pode ser útil, por exemplo, em casos onde o objeto original não exista mais quando a expressão lambda for executada[4:1].

#include <iostream>
using namespace std;

class A {
	public:
		int _valor{ 1 }; // Variável membro
		void funcao_membro()
		{
			auto funcao_lambda = [*this]() mutable -> void
			{ 
			// Repare que _valor pode ser acessado 
			// sem this, o this fica implícito.
				_valor++;
			};
			funcao_lambda();
		}

};

int main()
{
	setlocale(LC_ALL, "");
	A a;
	cout << "variável membro _valor antes: " << a._valor << endl;
	a.funcao_membro();
	cout << "variável membro _valor depois: " << a._valor << endl;
}
Saída em Console

Valor da variável membro _valor antes: 1
Valor da variável membro _valor depois: 1

Repare que os valores de *this foram copiados em um novo objeto como na passagem por valor.

Warning

Variáveis globais são sempre capturadas por referência, mesmo se forem marcadas por valor numa expressão lambda

Exemplo 07 - Variáveis globais

#include <iostream>
using namespace std;

int varivel_global{ 42 };
int main()
{
    auto funcao_lambda{ [=] { varivel_global = 13; } };
    funcao_lambda();
    cout << varivel_global;
}
Saída em Console

13

Mutable lambdas

Mutable remove o atributo const da função lambda, permite que valores capturados dentro do escopo sejam alterados.

É equivalente a

struct tipo_lambda_anonimo_mutable
{
	auto operator()(){}
};

Exemplo 08 - Efeitos colaterais do mutable

Repare que o mutable não alterou o valor da variável a que se encontra no escopo mais externo

#include <iostream>
using namespace std;

int main()
{
	auto a{0};
	auto funcao_lambda= [a]() mutable -> void
	{
		a++;
	};
	cout << "  Antes: " << a << endl;
	funcao_lambda();
	cout << " Depois: " << a << endl;
}
Saída em Console

Antes: 0
Depois: 0

Exemplo 09 - Efeitos colaterais do escopo & no mutable

Mais se usar & indicando que o escopo de a será capturado por referência o resultado será diferente

#include <iostream>
using namespace std;

int main()
{
	auto a{0};
	auto funcao_lambda= [&a]() mutable -> void
	{
		a++;
	};
	cout << "  Antes: " << a << endl;
	funcao_lambda();
	cout << " Depois: " << a << endl;
}
Saída em Console

Antes: 0
Depois: 1

Generic lambdas

A versão introduziu a possibilidade de atribuição de valores autos.

Exemplo 10

#include <iostream> 
using namespace std;
int main()
{
	const auto funcao_lambda = [](const auto& x) {cout << x; };
	funcao_lambda("Alo lambda");
}
Saída em Console

Alo lambda

A expressão lambda acima é equivalente a função anônima abaixo:

Exemplo 11

#include <iostream>
using namespace std;
struct funcao_anonima_generic
{
	template <typename T>
	auto operator()(const T& x) const
	{
		cout << x;
	}
};

int main()
{
	funcao_anonima_generic fag;
	fag("Alo lambda");

}

Saída em Console

Alo lambda

Se tiverem mais parâmetros, ele se desmembrará em múltiplos parâmetros templates.

Exemplo 12

#include <iostream>
using namespace std;
struct funcao_anonima_generic
{
	template <typename t0, typename t1, typename t2, typename t3>
	auto operator()(const t0& x, const t1& y, const t2& z, const t3& w) const
	{
		cout << x << y << z << w;
	}
};

int main()
{
	funcao_anonima_generic fag;
	fag("Alo lambda", 1, 2.0f, 3.2);

}
Saída em Console

Alo lambda123.2

Lambdas com templates variável genérico (generic variadic lambdas)

Lambdas podem ser criados com parâmetro variável

Exemplo 13

#include <iostream>
using namespace std;

int main()
{
	const auto log_error = [](auto ... xs) {logerror, xs ...; };

}

Constexpr lambdas

Exemplo 14

Uma expressão lambda pode retornar implicitamente um constexpr.

[] { return 5;}

é semanticamente equivalente a

[] () constexpr { return 5;}

Exemplo 15

Usando expressões lambda numa expressão constante.

#include <iostream>
#include <array>
using namespace std;
int main() {
static std::array<int, [] {return 5; }() > ints;
    for (auto valor : ints)
        cout << valor << ";";
}
Saída em Console

0;0;0;0;0;

Exemplo 16

Consteval lambdas

Uma expressão lambda também pode retornar implicitamente um consteval. Uma adição recente da da versão C++20.

#include <iostream>
using namespace std;
int main() {
    const int x = 10;
    auto lam = [](int x) consteval { return x + x; };
    cout << lam(x);
}
Saída em Console

20

Usando lambdas na programação funcional

Um dos usos mais

   #include <array>
   #include <format>
   #include <iostream>
   #include <numeric>

   int main() {

      constexpr std::array integers{ 1, 2, 3, 4 };
      std::cout << std::format("Somatório: {}\n",
      std::accumulatebegin(integers), std::end(integers, 0,
          [](const int& a,
              const int& b)
          {
              return a + b;
          }
      
      ));

O resultado seria

Pasted image 20221126212306.png

Referências Bibliográficas


  1. GREGOIRE, M. Professional C++. 5. ed. Indianapolis: John Wiley and Sons, 2020. ↩︎

  2. LAKOS, J. et al. Embracing modern C++ safely. First ed. Boston: Addison-Wesley, 2021. ↩︎ ↩︎ ↩︎

  3. SINGH, K.; IANCULESCU, A.; TORJE, L.-P. Design Patterns and Best Practices in Java: a Comprehensive Guide to Building Smart and Reusable Code in Java. Birmingham: Packt Publishing Ltd, 2018. }]: ↩︎

  4. GREGOIRE, M. Professional C++. 5. ed. Indianapolis: John Wiley and Sons, 2020. ↩︎ ↩︎

  5. O que significa stateful e stateless? Disponível em: <https://www.redhat.com/pt-br/topics/cloud-native-apps/stateful-vs-stateless>. Acesso em: 12 set. 2022. ↩︎

  6. ROMEO, V. Mastering C++ standard library features. Place of publication not identified: Packt, 2017. ↩︎