Funções Anônimas em C++

por Fabio A. Mazzarino

[] () { };

O trecho de código acima é válido para C++11, trata-se de uma função anônima, ou função lambda, e vamos entender melhor como funciona no post de hoje.

Funções anônimas, ou funções lambda, são funções sem nome, normalmente declaradas inline, e utilizadas através de ponteiros ou através de outros mecanismos que utilizam ponteiros de função, como por exemplo threads.

Sintaxe

O formato básico de uma função lambda é o seguinte:

[<capture>] (<arguments>) -> <return type> { <function body> }

capture, opcional, é um parâmetro que define quais partes do escopo atual pode ser acessado pela função. arguments são os argumentos da função, como em qualquer outra função são opcionais. return type, também opcional, é o tipo do retorno da função, se não for declarado será inferido pelo compilador (tipo auto).

Mas vamos exemplificar melhor

Declarando Uma Função Lambda

Como primeiro exemplo podemos definir uma função lambda para imprimir o conteúdo de cada elemento dentro de um std::vector utilizando std::for_each:

std::vector<unsigned> numbers;
for (int ct = 5; ct--; )
    numbers.push_back(ct);
std::for_each(tens.begin(), tens.end(),
    [] (unsigned n) { std::cout << "tens: " << n << std::endl; }
);

Que retorna a seguinte saída:

numbers: 4
numbers: 3
numbers: 2
numbers: 1
numbers: 0

Note o terceiro parâmetro da função std::for_each, é uma função lambda que recebe um único parâmetro, que imprime o valor corrente na saída padrão.

Declarando Tipo de Retorno

int nodds = std::count_if(numbers.begin(), numbers.end(),
    [](unsigned n) { return n % 2; }
);
int nevens = std::count_if(numbers.begin(), numbers.end(),
    [](unsigned n) { return !(n % 2); }
);
std::cout << "Total odds: " << nodds << std::endl;
std::cout << "Total evens: " << nevens << std::endl;

Que retorna a seguinte saída:

Total odds: 2
Total evens: 3

Depois de preencher o vetor com valores de 5 a 0, no exemplo acima a função std::count_if faz a totalização da quantidade de elementos pares e ímpares dentro do std::vector.

Declarando Captures

São cinco opções de captures. A capture vazia define que não haverá nenhuma variável externa acessível dentro da função. A capture = define que as variáveis acessíveis no escopo da declaração estarão disponíveis na função através de cópias de leitura das variáveis. A capture & define que as variáveis acessíveis no escopo da declaração estarão disponíveis na função através de referência às variáveis.

É possível também definir um conjunto de variáveis que estará disponível na função, listando-as dentro dos colchetes. Por último a capture this permite acesso a todos os atributos do objeto dentro da função.

Vamos a um exemplo que abrange os tipos de captures:

int x = 5;
auto f1 = [=]() -> int { return x; };
auto f2 = [&]() -> int { return x++; };
auto f3 = [x]() -> int { return x; };
auto f4 = [&x]() -> int { return ++x; };
std::cout << "f1(): " << f1() << std::endl;
std::cout << "x: " << x << std::endl;
std::cout << "f2(): " << f2() << std::endl;
std::cout << "x: " << x << std::endl;
std::cout << "f3(): " << f3() << std::endl;
std::cout << "x: " << x << std::endl;
std::cout << "f4(): " << f4() << std::endl;
std::cout << "x: " << x << std::endl;

Que entrega a saída:

f1(): 5
x: 5
f2(): 5
x: 6
f3(): 5
x: 6
f4(): 7
x: 7

Note que f1 e f3 utilizam variáveis por valor, somente leitura, enquanto que f2 e f4 utilizam variáveis por referência, leitura e escrita, o que permite que a variável x seja alterada.

Tipo da Função Lambda

Diferente do tipo de retorno uma função lambda tem um tipo, um ponteiro de função declarado no STL através de um template: std::function. Vamos a um bom exemplo:

std::map<std::string, std::function<int(int)> > ops;
ops["INCR"] = [] (int n) -> int { return n + 1; };
ops["DECR"] = [] (int n) -> int { return n - 1; };
ops["NEG"] = [] (int n) -> int { return -n; };
ops["DOUBLE"] = [] (int n) -> int { return n + n; };
ops["SQR"] = [] (int n) -> int { return n * n; };

std::cout << "INCR: " << ops["INCR"](5) << std::endl;
std::cout << "DECR: " << ops["DECR"](6) << std::endl;
std::cout << "NEG: " << ops["NEG"](1) << std::endl;
std::cout << "DOUBLE: " << ops["DOUBLE"](3) << std::endl;
std::cout << "SQR: " << ops["SQR"](4) << std::endl;

E a saída:

INCR: 6
DECR: 5
NEG: -1
DOUBLE: 6
SQR: 16

Apesar de estar usando const char* com constantes no índice do std::map, é perfeitamente plausível utilizar std::string advindas de outras funções, ou mesmo da entrada padrão. Ou seja, uma ótima forma de mapear nome de funções através de strings.

Quando Utilizar

Utilizar ou não funções lambdas é opção de cada equipe, seja a possibilidade de usar, ou as regras de quando usar. A grosso modo é sempre melhor não utilizar funções lambda quando forem muito extensas comparadas com o trecho de código aonde foi declarada, utilizá-las neste contexto só vai reduzir a visibilidade e dificultar a manutenção.