Fechamos a Fase 4 e, ao longo dela, usei dezenas de vezes uma construção que sempre prometi explicar: aquelas funções entre colchetes, [](int x){ return x*2; }, que passávamos aos algoritmos. Chamei-as de lambdas e pedi paciência. A paciência acaba agora. Abrimos a Fase 5 — a da genericidade, onde o código deixa de trabalhar com um tipo e passa a trabalhar com qualquer tipo — e as lambdas são a porta de entrada perfeita, porque você já as viu funcionar. Hoje você entende o que elas realmente são: funções escritas no local, sem nome, capazes de "capturar" variáveis do ambiente ao redor. Esse último poder — a captura — é o que as distingue de uma função comum e o que as torna indispensáveis. Ao fim da aula, aquele colchete misterioso terá revelado todos os seus segredos.
O que é uma lambda: uma função sem nome, escrita onde é usada
Em C, quando um algoritmo precisava de uma função (como o comparador do qsort), você era obrigado a defini-la em outro lugar, dar-lhe um nome, e passar um ponteiro para ela — mesmo que ela fosse usada uma única vez. A lambda elimina essa cerimônia: você escreve a função no exato ponto onde ela é necessária:
#include <iostream>
int main() {
// Uma lambda atribuída a uma variável, para vermos sua anatomia.
auto dobro = [](int x) { return x * 2; };
// ^^ ^^^^^^^ ^^^^^^^^^^^^^^
// captura parâmetros corpo
std::cout << dobro(5) << '\n'; // 10 — chama-se como uma função normal
std::cout << dobro(21) << '\n'; // 42
return 0;
}
A anatomia tem três partes. Os colchetes [] são a lista de captura (por ora vazia — já chegamos nela). Os parênteses (int x) são os parâmetros, como em qualquer função. As chaves { ... } são o corpo. O tipo de retorno é deduzido automaticamente (aqui, int), embora você possa especificá-lo com -> tipo quando precisar. Guardamos a lambda em auto porque seu tipo é gerado pelo compilador e não tem nome que você possa escrever — é o caso de uso do auto do artigo Adeus NULL, Bem-vindo à Dedução — nullptr e auto em sua forma mais pura.
A captura: o superpoder que uma função comum não tem
Até aqui, uma lambda parece só uma função escrita de forma compacta. O que a torna especial é a captura: a capacidade de acessar variáveis que existem no escopo onde ela foi definida. Aqueles colchetes [] são onde você diz quais variáveis do ambiente a lambda pode usar:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int limite = 10;
std::vector<int> v = {5, 12, 8, 20, 3};
// A lambda CAPTURA 'limite' por valor (o [limite] nos colchetes).
// Agora ela "carrega" uma cópia de limite e pode usá-lo lá dentro.
int acima = std::count_if(v.begin(), v.end(),
[limite](int x) { return x > limite; });
// ^^^^^^^ captura: a lambda enxerga 'limite'
std::cout << acima << " elementos acima de " << limite << '\n'; // 2
return 0;
}
Sem a captura, a lambda não saberia o que é limite — ele é uma variável local do main, não um parâmetro da lambda. Ao escrever [limite], você diz "leve uma cópia de limite com você". Isso é o que uma função comum não pode fazer: uma função nomeada só conhece seus parâmetros e variáveis globais; uma lambda pode "fotografar" o estado local ao seu redor. Uma lambda com captura é tecnicamente chamada de closure (fechamento), porque ela "fecha sobre" as variáveis que captura.
Capturar por valor versus por referência
Há duas formas de capturar, e a distinção é a mesma de passar argumentos (artigo Referências e Ponteiros, Frente a Frente), com as mesmas consequências. Capturar por valor ([x]) leva uma cópia — mudanças externas posteriores não afetam a lambda, e a lambda não afeta o original. Capturar por referência ([&x]) leva um vínculo ao original — a lambda vê mudanças e pode modificá-lo:
#include <iostream>
int main() {
int contador = 0;
// Captura POR VALOR: a lambda tem sua própria cópia congelada.
auto por_valor = [contador]() { return contador; };
// Captura POR REFERÊNCIA: a lambda acessa o 'contador' real.
auto incrementa = [&contador]() { ++contador; };
contador = 100; // muda o original DEPOIS de criar as lambdas
std::cout << por_valor() << '\n'; // 0 — capturou a cópia de quando valia 0
incrementa(); // mexe no contador real
incrementa();
std::cout << contador << '\n'; // 102 — a referência viu e alterou
return 0;
}
[contador] fotografou o valor 0 no momento da criação e o congelou; mudar contador depois não afeta a cópia dentro da lambda. [&contador] vinculou-se ao original, então incrementa() de fato altera o contador do main. Existem também as formas de captura padrão: [=] captura tudo o que for usado por valor, e [&] captura tudo por referência. Elas são convenientes, mas serei honesto sobre o risco no próximo bloco.
A armadilha honesta: captura por referência e tempo de vida
Prometo sempre o perigo, e o da captura por referência é sério porque é o dangling de novo, agora disfarçado. Uma lambda que captura por referência guarda um vínculo a uma variável; se essa variável morrer antes de a lambda ser chamada, o vínculo vira uma referência pendente. Isso é especialmente traiçoeiro quando você retorna uma lambda de uma função:
#include <functional>
// PERIGO: a lambda captura 'x' por referência, mas 'x' é local a criar_bug.
std::function<int()> criar_bug() {
int x = 42;
return [&x]() { return x; }; // x morre ao retornar; a referência fica pendente
}
// Chamar o resultado de criar_bug() lê memória inválida — comportamento indefinido.
A regra de bolso: capture por referência ([&x] ou [&]) apenas quando a lambda for usada imediatamente e localmente, dentro do mesmo escopo — como nos algoritmos, onde a lambda é consumida na hora. Se a lambda vai sobreviver ao escopo atual (ser guardada, retornada, usada depois), capture por valor ([x] ou [=]), para que ela leve suas próprias cópias e não dependa de variáveis que podem morrer. É a distinção posse-versus-observação da Fase 2 reaparecendo nas capturas: por valor, a lambda possui seus dados; por referência, ela apenas observa — e observar algo morto é fatal.
A captura é o que separa a lambda de uma função comum: ela leva consigo o ambiente em que foi escrita. Por valor, congela uma cópia no momento da criação; por referência, enxerga o original e reflete mudanças posteriores — e essa é a forma que cobra atenção, porque a referência não prolonga a vida de nada. Uma lambda que captura por referência e sobrevive ao escopo onde nasceu guarda referências penduradas, e o problema só aparece quando ela é finalmente chamada.
Fontes e leituras recomendadas
- cppreference.com/w/cpp/language/lambda: a referência completa sobre lambdas, listas de captura, o especificador
mutablee a dedução do tipo de retorno. - Bjarne Stroustrup, A Tour of C++ (3ª ed.), seções sobre lambdas: a apresentação das funções anônimas pelo criador da linguagem.
- Scott Meyers, Effective Modern C++ (2014), Itens 31–34 (sobre lambdas e capturas): a discussão definitiva dos perigos de captura por referência e por valor.
- cppreference.com/w/cpp/utility/functional/function: a referência de
std::function, o tipo que guarda qualquer "coisa chamável", incluindo lambdas. - ISO C++ Core Guidelines, regras F.50 ("Use a lambda when a function won't do") e F.52/F.53 (capturas por referência vs valor): as diretrizes sobre quando e como usar lambdas.
Exercícios
Exercício 1
Escreva uma lambda que receba dois int e devolva o maior deles. Atribua-a a uma variável auto e demonstre chamando-a com alguns pares.
Ver resposta
✓ Resposta: A lambda do máximo:
#include <iostream>
int main() {
auto maior = [](int a, int b) { return a > b ? a : b; };
std::cout << maior(3, 7) << '\n'; // 7
std::cout << maior(10, 2) << '\n'; // 10
return 0;
}
O tipo de retorno (int) é deduzido automaticamente do corpo.
Exercício 2
Dado um std::vector<int> e um valor int minimo lido do usuário, use std::count_if com uma lambda que capture minimo para contar quantos elementos são maiores ou iguais a ele.
Ver resposta
✓ Resposta: Contagem com captura:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {5, 12, 8, 20, 3};
int minimo;
std::cout << "mínimo: ";
std::cin >> minimo;
int quantos = std::count_if(v.begin(), v.end(),
[minimo](int x) { return x >= minimo; });
std::cout << quantos << " elementos >= " << minimo << '\n';
return 0;
}
A lambda captura minimo por valor para poder compará-lo com cada elemento — sem a captura, minimo seria desconhecido dentro dela.
Exercício 3
Explique a diferença de comportamento entre [x] e [&x] numa lambda, usando um exemplo em que modificar x após a criação da lambda produz resultados diferentes nas duas formas.
Ver resposta
✓ Resposta: [x] captura uma cópia de x no momento da criação da lambda, congelando-a; [&x] captura uma referência ao x original, que reflete mudanças posteriores. Exemplo:
#include <iostream>
int main() {
int x = 1;
auto copia = [x]() { return x; }; // fotografa x = 1
auto ref = [&x]() { return x; }; // vincula-se ao x real
x = 99; // muda x DEPOIS
std::cout << copia() << '\n'; // 1 — a cópia não vê a mudança
std::cout << ref() << '\n'; // 99 — a referência vê
return 0;
}
copia() devolve 1 porque congelou o valor original; ref() devolve 99 porque acessa o x atual. É a mesma distinção de valor versus referência que rege parâmetros de função.
Exercício 4
O código abaixo tem um bug de tempo de vida com captura por referência. Aponte-o e corrija trocando a estratégia de captura, explicando por que a correção resolve.
#include <functional>
#include <vector>
std::vector<std::function<int()>> fabricas;
void registra(int n) {
fabricas.push_back([&n]() { return n * 10; }); // guarda a lambda para depois
}
Ver resposta
✓ Resposta: O bug: [&n] captura o parâmetro n por referência, mas n é local a registra e morre quando a função retorna. As lambdas guardadas em fabricas ficam com referências pendentes; chamá-las depois lê memória inválida. Correção: capturar por valor, para que cada lambda leve sua própria cópia de n:
#include <functional>
#include <vector>
std::vector<std::function<int()>> fabricas;
void registra(int n) {
fabricas.push_back([n]() { return n * 10; }); // captura por valor: cópia própria
}
A correção resolve porque, com [n], cada lambda possui sua própria cópia de n, independente da variável local que morre — a lambda não depende mais de nada que possa deixar de existir. É a regra da aula: lambdas que sobrevivem ao escopo devem capturar por valor.
Exercício 5
Use uma lambda com captura por referência para somar os elementos de um std::vector<int> num acumulador externo, percorrendo com std::for_each. Depois explique por que, neste caso específico, a captura por referência é segura, ligando à regra de tempo de vida da aula.
Ver resposta
✓ Resposta: Soma com captura por referência:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> v = {1, 2, 3, 4};
int soma = 0;
// for_each aplica a lambda a cada elemento; ela acumula em 'soma' por referência.
std::for_each(v.begin(), v.end(), [&soma](int x) { soma += x; });
std::cout << "soma: " << soma << '\n'; // 10
return 0;
}
A captura por referência é segura aqui porque a lambda é usada imediatamente e localmente: std::for_each a executa na mesma hora, dentro do mesmo escopo onde soma ainda está viva, e a lambda não sobrevive à chamada nem é guardada em lugar nenhum. Como soma existe durante todo o percurso e a lambda é descartada logo após, não há risco de referência pendente — exatamente o caso em que a regra da aula permite a captura por referência. (Na prática, std::accumulate seria mais idiomático para somar, mas o exemplo ilustra a captura segura por referência.)