Funções que você escreve onde as usa
Na página anterior você viu como a sobrecarga permite que várias funções compartilhem um nome. Mas às vezes você não quer uma função nomeada de jeito nenhum: você precisa de um pedacinho de lógica uma única vez, bem onde a usa, e dar um nome a ela só adicionaria poluição. É isso que uma lambda é: uma função anônima que você pode definir inline.
Uma lambda tem um formato característico de quatro partes:
[capture](parameters) -> return_type { body }
O [] é o sinal de que você está diante de uma lambda. O tipo de retorno é opcional -normalmente o compilador o deduz. Esta é a mais simples possível:
greet é apenas uma variável (cujo tipo é impronunciável, então você a guarda com auto) que você pode chamar com (). Lambdas com parâmetros funcionam exatamente como funções normais:
Capturas: alcançando o escopo ao redor
A parte que faz as lambdas serem mais do que funções sem nome é a lista de captura -o []. Ela permite que a lambda use variáveis do escopo onde foi definida, não apenas seus próprios parâmetros.
Capture por valor com [x]: a lambda recebe sua própria cópia, congelada no momento em que a lambda é criada.
Repare que scale(5) imprimiu 50, usando o valor de factor igual a 10 que existia quando a lambda foi criada. Capturar por valor tira um instantâneo.
Capture por referência com [&x]: a lambda se refere à variável original, enxerga mudanças posteriores e pode modificá-la.
Você também pode capturar tudo o que a lambda usa com [=] (tudo por valor) ou [&] (tudo por referência). Eles são convenientes, mas ser explícito -[total] ou [&total]- documenta exatamente o que a lambda toca e é mais fácil de raciocinar.
A armadilha da referência pendente
Capturar por referência é poderoso e perigoso em igual medida. A referência só é válida enquanto a variável original estiver viva. Se a lambda viver mais que o que ela capturou, você obtém uma referência pendente e comportamento indefinido -o programa pode travar, pode imprimir lixo, pode parecer funcionar por acidente.
Este é o erro clássico: retornar uma lambda que captura uma variável local por referência.
auto makeCounter() {
int count = 0;
return [&count]() { return ++count; }; // BUG: count morre aqui
}
// A lambda retornada agora referencia memória destruída.
Quando makeCounter retorna, sua variável local count é destruída, mas a lambda ainda mantém uma referência a ela. Chamar a lambda retornada toca memória morta. A correção é capturar por valor para que a lambda seja dona do seu próprio estado:
Regra prática: capture por referência apenas quando a lambda é usada imediatamente e localmente (como com os algoritmos abaixo). No momento em que uma lambda é armazenada, retornada ou executada mais tarde, prefira capturar por valor.
mutable e tipos de retorno
Você reparou no mutable daquele último exemplo? Por padrão, uma captura por valor é const dentro da lambda -você pode ler a cópia, mas não alterá-la. Adicionar mutable permite que a lambda modifique suas próprias cópias entre chamadas.
mutable só afeta a cópia privada da lambda -o seen de fora fica intocado, que é justamente o sentido de capturar por valor.
Na maioria das vezes o compilador deduz o tipo de retorno sem problemas. Você só precisa especificá-lo com -> quando há ambiguidade, como uma lambda que poderia retornar tipos diferentes em ramos diferentes:
// Sem -> o compilador não consegue decidir entre int e double
auto half = [](int n) -> double {
if (n % 2 == 0) return n / 2; // int
return n / 2.0; // double
};
Lambdas e algoritmos: a verdadeira recompensa
A razão pela qual as lambdas foram adicionadas ao C++ é alimentar pequenos pedaços de lógica para os algoritmos da biblioteca padrão. Antes das lambdas, você tinha que escrever uma função nomeada separada ou um objeto função desajeitado longe de onde ele era usado. Agora a lógica vive bem no ponto da chamada.
O exemplo mais comum é uma ordem de classificação personalizada:
As capturas brilham aqui porque a lambda pode trazer um valor para filtrar ou contar em relação a ele. Isto conta quantos números ultrapassam um limite escolhido pelo usuário:
Como essas lambdas são usadas imediatamente e não vivem mais que a função ao redor, capturar por referência ([&passMark]) também seria seguro aqui -mas por valor é igualmente claro e nunca fica pendente.
Próximo: Ponteiros
As lambdas levantaram silenciosamente uma questão mais profunda: quando você captura [&x], a lambda está se agarrando à localização de x, e essa localização permanece válida apenas enquanto x viver. Essa ideia -um valor que se refere a onde algo vive na memória, e o que acontece quando aquilo para o qual ele aponta desaparece- é exatamente do que trata a próxima página. Vamos encarar de frente os ponteiros: como pegar um endereço, como segui-lo e como o mesmo problema de referência pendente que você acabou de ver aparece por todo o C++.
Perguntas frequentes
O que é uma lambda em C++?
Uma lambda é uma função anônima que você pode escrever inline, bem onde a usa. A sintaxe é [capturas](parâmetros){ corpo }. Ela é perfeita para operações curtas e pontuais -como o comparador que você passa para std::sort- sem precisar declarar uma função nomeada separada em outro lugar.
Qual é a diferença entre capturar por valor e por referência em uma lambda de C++?
[x] captura uma cópia de x congelada no momento em que a lambda é criada. [&x] captura uma referência ao x original, então a lambda enxerga mudanças posteriores e pode modificá-lo. Use [&] apenas enquanto as variáveis capturadas tiverem garantia de viver mais que a lambda, ou você terá uma referência pendente.
Por que minha lambda de C++ diz que não consegue modificar uma variável capturada?
Capturas por valor são const dentro da lambda por padrão. Adicione a palavra-chave mutable -[x]() mutable { x++; }- para permitir que a lambda altere sua própria cópia. Note que isso só muda a cópia da lambda, não a variável original de fora.