C++ Lambdas
Às vezes você precisa de uma função pequena, de uma linha, que só vai ser usada num lugar: "compare dois alunos pela nota", "diga se esta nota é de aprovado". Criar uma função com nome lá em cima do arquivo para isso é exagero. A lambda C++11 é uma função sem nome, escrita no meio do código, exatamente onde é usada. Nesta aula você vai entender cada pedaço de [](int x) { return x * 2; }, guardar lambdas em variáveis, deixar a lambda usar variáveis de fora (a captura) e, o mais importante, entregar lambdas para as funções da biblioteca padrão, como std::sort, para ordenar os alunos da Ana por nota.
- A anatomia de uma lambda:
[],()e{}. - Como guardar uma lambda numa variável com
autoe chamá-la. - Como a lambda usa variáveis de fora:
[x](cópia),[&x](referência),[=]e[&], e o erro‘desconto’ is not captured. - O que
mutablefaz (só para reconhecer). - Como passar lambdas para
std::sortestd::count_if. - A ideia do
std::function, um tipo para guardar "qualquer função".
Todos os programas desta aula foram compilados com o g++ 15 na máquina fictícia da Ana, na pasta ~/cpp, com o comando padrão do guia, g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: o bilhete na hora
Quando a Ana sai e deixa o filho com a avó, ela não escreve um manual com capa e índice: escreve um bilhete rápido, ali na hora, e entrega: "esquentar a sopa às 19h". O bilhete não tem título, só serve para aquela situação, e pode mencionar coisas da casa ("a sopa que está na geladeira").
Uma lambda é o bilhete do C++: uma função curta, sem nome, escrita na hora e entregue para quem vai usá-la. E, como o bilhete, ela pode mencionar coisas que estão em volta, as variáveis do lugar onde foi escrita.
1. A primeira lambda
#include <iostream>
int main() {
auto dobro = [](int x) { return x * 2; };
std::cout << dobro(5) << "\n";
std::cout << dobro(21) << "\n";
}
- []
- A lista de captura: é o que avisa "aqui começa uma lambda". Entre os colchetes vão as variáveis de fora que a lambda pode usar (seção 3). Vazia: a lambda não usa nada de fora.
- (int x)
- Os parâmetros, iguais aos de uma função comum (aula Parâmetros).
- { return x * 2; }
- O corpo, igual ao de uma função. O tipo de retorno é deduzido do
return, como noautoda aula Retorno: aqui,int. - auto dobro = … ;
- A lambda é um valor, e pode ser guardada numa variável. O tipo dela é um nome inventado pelo compilador, que ninguém consegue escrever; por isso o
autoé obrigatório (aula auto). Repare no;no fim: é uma linha de criação de variável, comoint x = 5;. - dobro(5)
- Chama a lambda como se fosse uma função comum.
$ g++ -std=c++23 -Wall -Wextra primeira.cpp -o primeira $ ./primeira 10 42
Compare com a função da aula Retorno:
| Função comum | Lambda guardada em variável |
|---|---|
int dobro(int x) { return x * 2; } | auto dobro = [](int x) { return x * 2; }; |
| Escrita fora de qualquer função, no topo do arquivo | Escrita dentro de uma função, onde for preciso |
| Tipo de retorno escrito antes do nome | Tipo de retorno deduzido (ou escrito com ->) |
| Só enxerga globais e os próprios parâmetros | Pode capturar variáveis locais de onde foi criada |
Lembra que, na aula Funções, escrever uma função dentro do main dava erro (a function-definition is not allowed here)? A lambda é o jeito permitido de fazer isso.
Mais formas
#include <iostream>
#include <string>
int main() {
auto saudar = []() { std::cout << "Olá!\n"; };
auto soma = [](int a, int b) { return a + b; };
auto media = [](double a, double b) -> double { return (a + b) / 2; };
auto rotulo = [](const std::string& nome, double nota) {
return nome + ": " + std::to_string(nota);
};
saudar();
std::cout << soma(2, 3) << "\n";
std::cout << media(7, 8) << "\n";
std::cout << rotulo("Bruno", 7.5) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra varios.cpp -o varios $ ./varios Olá! 5 7.5 Bruno: 7.500000 « o std::to_string sempre mostra 6 casas
| Lambda | O que mostra |
|---|---|
[]() { … } | Sem parâmetros e sem return: é como uma função void. (Os () vazios podem até ser omitidos: [] { … }.) |
[](int a, int b) { … } | Vários parâmetros. |
[](double a, double b) -> double { … } | Tipo de retorno escrito com -> tipo, depois dos parâmetros. Útil quando os return dariam tipos diferentes. |
[](const std::string& nome, …) { … } | Parâmetros por const&, como em qualquer função (aula Por Valor × Por Referência). O corpo pode ter várias linhas. |
Experimente: crie uma lambda triplo e mostre triplo(7). Depois crie uma lambda ehPar que recebe um int e devolve true se ele for par, e use-a num if.
#include <iostream>
int main() {
auto dobro = [](int x) { return x * 2; };
std::cout << dobro(5) << "\n";
}
2. O erro mais comum: o ; do fim
Como a lambda termina com }, é fácil esquecer que a linha inteira é uma criação de variável e precisa de ;:
#include <iostream>
int main() {
auto dobro = [](int x) { return x * 2; }
std::cout << dobro(5) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra semponto.cpp -o semponto
semponto.cpp: In function ‘int main()’:
semponto.cpp:5:5: error: expected ‘,’ or ‘;’ before ‘std’
5 | std::cout << dobro(5) << "\n";
| ^~~
"Esperava , ou ; antes de std". Como sempre com ; esquecido, o erro aponta a linha seguinte. Olhe o fim da linha anterior: } sem ;.
3. Captura: usar variáveis de fora
A Ana quer uma lambda que aplica o desconto do dia, que está numa variável do main. Primeira tentativa:
#include <iostream>
int main() {
double desconto = 0.1;
auto comDesconto = [](double preco) { return preco * (1 - desconto); };
std::cout << comDesconto(50) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra semcaptura.cpp -o semcaptura
semcaptura.cpp: In lambda function:
semcaptura.cpp:5:63: error: ‘desconto’ is not captured
5 | auto comDesconto = [](double preco) { return preco * (1 - desconto); };
| ^~~~~~~~
semcaptura.cpp:5:25: note: the lambda has no capture-default
5 | auto comDesconto = [](double preco) { return preco * (1 - desconto); };
| ^
semcaptura.cpp:4:12: note: ‘double desconto’ declared here
4 | double desconto = 0.1;
| ^~~~~~~~
- In lambda function
- "Dentro da função lambda": o problema está no corpo da lambda.
- ‘desconto’ is not captured
- "‘desconto’ não foi capturado". A variável existe, no
main, mas a lambda não pediu para usá-la. - note: the lambda has no capture-default
- "A lambda não tem captura padrão": os colchetes estão vazios, então ela não leva nada de fora (o
[=]e o[&], mais abaixo, são as "capturas padrão"). - note: ‘double desconto’ declared here
- Onde a variável que faltou foi criada.
Uma lambda é como uma função à parte (lembre da aula Escopo): por padrão, ela não enxerga as variáveis locais de quem a criou. Para usar, é preciso capturar, escrevendo o nome entre os colchetes:
#include <iostream>
int main() {
double desconto = 0.1;
auto comDesconto = [desconto](double preco) { return preco * (1 - desconto); };
std::cout << comDesconto(50) << "\n";
std::cout << comDesconto(20) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra captura.cpp -o captura $ ./captura 45 18
Missão: a lambda total precisa somar o frete ao preço, mas o programa não compila (no editor, o Clang diz variable 'frete' cannot be implicitly captured e ainda sugere as correções numa lista de notas). Capture o frete para aparecer Total com frete: 65.
#include <iostream>
int main() {
double frete = 15;
auto total = [](double preco) { return preco + frete; };
std::cout << "Total com frete: " << total(50) << "\n";
}
#include <iostream>
int main() {
double frete = 15;
auto total = [frete](double preco) { return preco + frete; };
std::cout << "Total com frete: " << total(50) << "\n";
}
Por cópia [x] ou por referência [&x]
É a mesma escolha da aula Por Valor × Por Referência. [desconto] guarda uma cópia do valor no momento em que a lambda é criada. [&desconto] guarda uma referência: a lambda sempre olha a variável original. A diferença aparece quando a variável muda depois:
#include <iostream>
int main() {
double desconto = 0.1;
auto porCopia = [desconto](double preco) { return preco * (1 - desconto); };
auto porReferencia = [&desconto](double preco) { return preco * (1 - desconto); };
desconto = 0.5;
std::cout << "cópia: " << porCopia(100) << "\n";
std::cout << "referência: " << porReferencia(100) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra copiaref.cpp -o copiaref $ ./copiaref cópia: 90 « ainda usa o 0.1 da hora em que foi criada referência: 50 « vê o 0.5 atual
Outra diferença: uma variável capturada por cópia é somente leitura dentro da lambda. Para a lambda mudar uma variável de fora, como um contador de vendas, a captura tem de ser por referência:
#include <iostream>
int main() {
int vendas = 0;
auto vender = [vendas]() { vendas++; };
vender();
std::cout << vendas << "\n";
}
$ g++ -std=c++23 -Wall -Wextra mudar.cpp -o mudar
mudar.cpp: In lambda function:
mudar.cpp:5:32: error: increment of read-only variable ‘vendas’
5 | auto vender = [vendas]() { vendas++; };
| ^~~~~~
"Incremento de variável somente leitura ‘vendas’". Com [&vendas]:
#include <iostream>
int main() {
int vendas = 0;
auto vender = [&vendas]() { vendas++; };
vender();
vender();
vender();
std::cout << "Vendas: " << vendas << "\n";
}
$ g++ -std=c++23 -Wall -Wextra mudarref.cpp -o mudarref $ ./mudarref Vendas: 3
Capturar tudo: [=] e [&]
Em vez de listar cada variável, você pode capturar todas as que a lambda usar: [=] captura tudo por cópia; [&], tudo por referência.
#include <iostream>
#include <string>
int main() {
std::string loja = "Lojinha da Ana";
double desconto = 0.1;
int vendas = 0;
auto etiqueta = [=](double preco) {
std::cout << loja << ": R$ " << preco * (1 - desconto) << "\n";
};
auto vender = [&](double preco) {
vendas++;
std::cout << "venda " << vendas << " de R$ " << preco * (1 - desconto) << "\n";
};
etiqueta(30);
vender(30);
vender(10);
std::cout << "Total de vendas: " << vendas << "\n";
}
$ g++ -std=c++23 -Wall -Wextra todas.cpp -o todas $ ./todas Lojinha da Ana: R$ 27 venda 1 de R$ 27 venda 2 de R$ 9 Total de vendas: 2
| Captura | O que a lambda leva | Pode mudar a variável de fora? |
|---|---|---|
[] | Nada | — |
[x] | Uma cópia de x, tirada na criação | Não |
[&x] | Uma referência a x | Sim |
[x, &y] | Cópia de x e referência a y | Só y |
[=] | Cópia de tudo o que usar | Não |
[&] | Referência a tudo o que usar | Sim |
[&, x] / [=, &y] | Tudo de um jeito, com exceções | Conforme cada uma |
Qual usar? Prefira listar as variáveis ([desconto], [&vendas]): quem lê vê na hora o que a lambda usa e o que ela pode mudar. [&] é prático para lambdas curtas usadas na mesma linha (como as do std::sort, abaixo). Cuidado com referências que duram mais que a variável: uma lambda com [&] guardada e chamada depois que a variável já morreu é o mesmo problema da referência pendurada.
mutable: só para reconhecer
Se você quer mudar a cópia dentro da lambda (sem afetar a variável de fora), escreva mutable depois dos parâmetros: [vendas]() mutable { vendas++; }. A cópia passa a ser da lambda e lembra o valor entre chamadas, como um static (aula Escopo). Num teste da Ana, duas chamadas mostraram 1 e 2 dentro da lambda, e a vendas de fora continuou 0. É raro no dia a dia; quando quiser mudar a variável de fora, use [&vendas].
4. Onde as lambdas brilham: entregar para outras funções
Guardar uma lambda numa variável é bom para aprender. Mas o uso principal é outro: entregar a lambda, na hora, para uma função que precisa saber "como fazer" alguma coisa. As funções de <algorithm> (aula Algoritmos) funcionam assim.
Ordenar com std::sort
std::sort ordena um vetor. Sem lambda, ele ordena do menor para o maior. Com uma lambda, você diz como comparar:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<double> notas = {7.5, 4, 9, 6, 5.5};
std::sort(notas.begin(), notas.end());
for (double n : notas) std::cout << n << " ";
std::cout << "\n";
std::sort(notas.begin(), notas.end(), [](double a, double b) { return a > b; });
for (double n : notas) std::cout << n << " ";
std::cout << "\n";
}
- #include <algorithm>
- Onde moram
std::sort,std::count_ife muitos outros. - notas.begin(), notas.end()
- O começo e o fim do trecho a ordenar: aqui, o vetor inteiro (Iteradores explica o que são).
- [](double a, double b) { return a > b; }
- A regra de comparação: recebe dois elementos e devolve
trueseadeve vir antes deb. Coma > b, o maior vem antes: ordem decrescente. Ostd::sortchama essa lambda várias vezes, com vários pares, enquanto ordena.
$ g++ -std=c++23 -Wall -Wextra ordenar.cpp -o ordenar $ ./ordenar 4 5.5 6 7.5 9 « sem lambda: crescente 9 7.5 6 5.5 4 « com a lambda a > b: decrescente
Ordenar os alunos da Ana por nota
Agora o caso real: a Ana tem uma turma, cada aluno com nome e nota, e quer o ranking. Para guardar nome e nota juntos, ela usa uma struct, uma "ficha" com dois campos (aula Structs; aqui basta saber que aluno.nome e aluno.nota leem cada campo). O std::sort não sabe comparar fichas sozinho; a lambda ensina:
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Aluno {
std::string nome;
double nota;
};
int main() {
std::vector<Aluno> turma = {
{"Bruno", 7.5}, {"Carla", 4}, {"Davi", 9}, {"Elis", 6}, {"Fábio", 5.5}
};
std::sort(turma.begin(), turma.end(), [](const Aluno& a, const Aluno& b) {
return a.nota > b.nota;
});
for (const Aluno& aluno : turma) {
std::cout << aluno.nome << ": " << aluno.nota << "\n";
}
double media = 6;
long aprovados = std::count_if(turma.begin(), turma.end(), [media](const Aluno& a) {
return a.nota >= media;
});
std::cout << aprovados << " aprovados\n";
}
$ g++ -std=c++23 -Wall -Wextra alunos.cpp -o alunos $ ./alunos Davi: 9 Bruno: 7.5 Elis: 6 Fábio: 5.5 Carla: 4 3 aprovados
- [](const Aluno& a, const Aluno& b) { return a.nota > b.nota; }
- "O aluno
avem antes dobse a nota dele for maior". Os parâmetros sãoconst¶ não copiar as fichas a cada comparação. Para ordenar por nome, bastariareturn a.nome < b.nome;. - std::count_if(…, [media](const Aluno& a) { … })
count_if("conte se") passa cada elemento para a lambda e conta quantas vezes ela devolveutrue. Esta lambda captura amediadomain: é aqui que a captura mostra seu valor, porque ocount_ifsó passa um argumento (o aluno), e a nota de corte precisa vir de fora.
Sem lambdas, a Ana teria de escrever, lá no topo do arquivo, uma função bool notaMaior(const Aluno& a, const Aluno& b) só para usar uma vez, e não conseguiria passar a media para o count_if sem uma variável global. Com a lambda, a regra fica exatamente onde é usada.
5. std::function: só a ideia
Cada lambda tem um tipo próprio, sem nome, e é por isso que usamos auto. Mas e se você quiser um parâmetro que aceite "qualquer função que recebe um double e devolve um double"? Para isso existe std::function, de <functional>:
#include <functional>
#include <iostream>
double aplicar(double preco, std::function<double(double)> regra) {
return regra(preco);
}
int main() {
std::function<double(double)> promocao = [](double p) { return p * 0.9; };
std::function<double(double)> frete = [](double p) { return p + 15; };
std::cout << aplicar(100, promocao) << "\n";
std::cout << aplicar(100, frete) << "\n";
std::cout << aplicar(100, [](double p) { return p; }) << "\n";
}
$ g++ -std=c++23 -Wall -Wextra function.cpp -o function $ ./function 90 115 100
std::function<double(double)> lê-se "uma função que recebe um double e devolve um double". Dentro dos < >, vem o tipo de retorno e, entre parênteses, os tipos dos parâmetros. A função aplicar não sabe qual regra vai receber; quem chama decide. Isso permite, por exemplo, guardar várias regras num std::vector<std::function<…>>. Ele tem um pequeno custo de desempenho em relação ao auto; nas funções da biblioteca, como o std::sort, quem faz esse trabalho são os templates, sem custo extra.
Quando dá errado
error: ‘desconto’ is not captured
O que significa: a lambda usa uma variável local de fora que não está entre os colchetes. A nota the lambda has no capture-default lembra que os colchetes estão vazios.
Como consertar: capture a variável: [desconto] para ler uma cópia, [&desconto] para ver mudanças ou alterar o original. (Variáveis globais e constantes constexpr não precisam de captura.)
error: increment of read-only variable (ou assignment of read-only variable) dentro da lambda
O que significa: a lambda capturou por cópia ([x] ou [=]) e tentou mudar a variável. Cópias capturadas são somente leitura. No editor, o Clang diz cannot assign to a variable captured by copy in a non-mutable lambda.
Como consertar: se a ideia é mudar a variável de fora, capture por referência: [&x]. Se a ideia é só mexer numa cópia interna, use mutable.
error: expected ‘,’ or ‘;’ before … na linha depois da lambda
O que significa: faltou o ; depois do } da lambda guardada em variável.
Como consertar: auto f = [](int x) { return x * 2; }; — com ; no fim.
O que significa: a captura foi por cópia ([x] ou [=]), e a cópia é tirada quando a lambda é criada, não quando é chamada.
Como consertar: capture por referência ([&x]), ou crie a lambda depois de dar o valor final à variável.
std::sort ordenou ao contrário
O que significa: a lambda de comparação deve devolver true quando o primeiro deve vir antes. a < b dá ordem crescente; a > b, decrescente.
Como consertar: troque o sinal. E nunca use <= ou >= numa comparação do std::sort: com elementos iguais, isso quebra as regras que ele espera e pode até derrubar o programa.
Referência rápida
| Escrita | O que faz |
|---|---|
[](int x) { return x * 2; } | Lambda sem captura, um parâmetro, retorno deduzido |
auto f = [](…) { … }; | Guarda a lambda numa variável; chame com f(…) |
[](double a) -> double { … } | Tipo de retorno explícito |
[x] / [&x] | Captura x por cópia / por referência |
[=] / [&] | Captura tudo o que usar, por cópia / por referência |
[x]() mutable { … } | Permite mudar a cópia capturada |
std::sort(v.begin(), v.end(), [](…a, …b) { return …; }) | Ordena com a regra da lambda (true = a antes de b) |
std::count_if(v.begin(), v.end(), [](…x) { return …; }) | Conta os elementos para os quais a lambda devolve true |
std::function<R(A)> | Tipo que guarda qualquer função que recebe A e devolve R |
Exercícios
1. Completar a lambda
Complete a lambda que devolve o quadrado de um número:
2. Capturar por referência
Complete a captura para que a lambda possa aumentar a variável pontos do main:
3. Ordem crescente
Complete a regra para o std::sort ordenar os preços do menor para o maior:
4. int x = 1; auto f = [x]() { return x; }; x = 5; Quanto vale f()?
5. Por que guardamos lambdas em auto?
6. Para contar quantas notas de um vetor são maiores que um corte guardado numa variável, qual chamada está certa?
Missão final: a vitrine da lojinha
Missão: complete as duas chamadas com lambdas. O std::sort deve ordenar os produtos do mais barato para o mais caro. O std::count_if deve contar quantos custam até limite (capture a variável). Saída esperada:
caneta: R$ 2.5 borracha: R$ 3 caderno: R$ 12 mochila: R$ 89.9 2 produtos até R$ 10
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Produto {
std::string nome;
double preco;
};
int main() {
std::vector<Produto> vitrine = {
{"caderno", 12}, {"caneta", 2.5}, {"mochila", 89.9}, {"borracha", 3}
};
// std::sort(vitrine.begin(), vitrine.end(), /* sua lambda aqui */);
for (const Produto& p : vitrine) {
std::cout << p.nome << ": R$ " << p.preco << "\n";
}
double limite = 10;
long baratos = 0; // troque o 0 por std::count_if(...) com uma lambda
std::cout << baratos << " produtos até R$ " << limite << "\n";
}
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct Produto {
std::string nome;
double preco;
};
int main() {
std::vector<Produto> vitrine = {
{"caderno", 12}, {"caneta", 2.5}, {"mochila", 89.9}, {"borracha", 3}
};
std::sort(vitrine.begin(), vitrine.end(), [](const Produto& a, const Produto& b) {
return a.preco < b.preco;
});
for (const Produto& p : vitrine) {
std::cout << p.nome << ": R$ " << p.preco << "\n";
}
double limite = 10;
long baratos = std::count_if(vitrine.begin(), vitrine.end(), [limite](const Produto& p) {
return p.preco <= limite;
});
std::cout << baratos << " produtos até R$ " << limite << "\n";
}
Resumo
- Uma lambda é uma função sem nome, escrita onde é usada:
[captura](parâmetros) { corpo }. - Guarde-a com
auto f = …;(não esqueça o;) e chame comf(…). O retorno é deduzido; para escrever, use-> tipo. - Para usar variáveis locais de fora, capture:
[x](cópia, tirada na criação, somente leitura),[&x](referência, vê e muda o original),[=]e[&](tudo). Sem captura:‘x’ is not captured. mutabledeixa mudar a cópia capturada; é raro.- O grande uso: entregar a regra para os algoritmos.
std::sortcom[](a, b) { return a.nota > b.nota; }ordena a turma por nota;std::count_ifcom[media](a) { … }conta os aprovados. std::function<R(A)>guarda qualquer função com aquela forma, útil como parâmetro.- Este é o fim do grupo Funções. O próximo grupo começa em Referências, que aprofunda o
&.