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.

Você vai aprender
  • A anatomia de uma lambda: [], () e {}.
  • Como guardar uma lambda numa variável com auto e 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 mutable faz (só para reconhecer).
  • Como passar lambdas para std::sort e std::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 no auto da 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, como int 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 comumLambda 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 arquivoEscrita dentro de uma função, onde for preciso
Tipo de retorno escrito antes do nomeTipo de retorno deduzido (ou escrito com ->)
Só enxerga globais e os próprios parâmetrosPode 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
LambdaO 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
CapturaO que a lambda levaPode mudar a variável de fora?
[]Nada—
[x]Uma cópia de x, tirada na criaçãoNão
[&x]Uma referência a xSim
[x, &y]Cópia de x e referência a ySó y
[=]Cópia de tudo o que usarNão
[&]Referência a tudo o que usarSim
[&, x] / [=, &y]Tudo de um jeito, com exceçõesConforme 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_if e 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 true se a deve vir antes de b. Com a > b, o maior vem antes: ordem decrescente. O std::sort chama 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 a vem antes do b se a nota dele for maior". Os parâmetros são const& para não copiar as fichas a cada comparação. Para ordenar por nome, bastaria return 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 devolveu true. Esta lambda captura a media do main: é aqui que a captura mostra seu valor, porque o count_if só 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.

A lambda usou o valor antigo da variável

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.

O 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

EscritaO 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:

auto quadrado = (int x) { return x * x; };

2. Capturar por referência

Complete a captura para que a lambda possa aumentar a variável pontos do main:

auto marcar = []() { pontos += 10; };

3. Ordem crescente

Complete a regra para o std::sort ordenar os preços do menor para o maior:

[](double a, double b) { return a b; }

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 com f(…). 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.
  • mutable deixa mudar a cópia capturada; é raro.
  • O grande uso: entregar a regra para os algoritmos. std::sort com [](a, b) { return a.nota > b.nota; } ordena a turma por nota; std::count_if com [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 &.