paulo1205
(usa Ubuntu)
Enviado em 11/05/2016 - 20:13h
Permita-me fazer uma explicação progressiva.
Já usou a função
qsort () da biblioteca padrão do C? Ela implementa o algoritmo Quick Sort sobre um array genérico de elementos de quaisquer tipos.
Como pode uma função ser usada para ordenar dados de qualquer tipo? Bom, na verdade ela delega para você, por meio de uma função de
call-back , fazer a comparação que vai determinar, entre dois elementos, qual deve ser disposto antes do outro. Veja o seguinte código.
#include <stdlib.h>
#include <string.h>
/* Tipo de dados com um registro qualquer. */
struct registro {
char nome[50];
time_t nascimento;
};
/* Estrutura usada para representar arrays dinâmicos de registros. */
struct catalogo {
struct registro *base;
size_t n_regs;
};
/*
Funções call-back de comparação. Argumentos têm de ser “void *”
porque é isso que qsort() exige. Dentro de cada função, convertemos
de volta para “struct registro *”.
*/
int compara_registro_nome(void *pr1, void *pr2){
struct registro *preg1=pr1, *preg2=pr2;
return strcoll(preg1->nome, preg2->nome);
}
int compara_registro_nascimento(void *pr1, void *pr2){
struct registro *preg1=pr1, *preg2=pr2;
return preg1->nascimento - preg2->nascimento;
}
/*
Funções que ordenam catálogos. Note que, dentro delas, eu não chamo as
funções de comparação, mas apenas as passo como ponteiros de função para
que qsort() as invoque nos momentos adequados.
*/
void ordena_catalogo_nome(struct catalogo *pcat){
qsort(pcat->base, pcat->n_regs, sizeof *pcat->base, compara_registro_nome);
}
void ordena_catalogo_nascimento(struct catalogo *pcat){
qsort(pcat->base, pcat->n_regs, sizeof *pcat->base, compara_registro_nascimento);
}
Funções de call-back são coisas muito úteis e que existem há muito tempo.
A biblioteca do C++ oferece uma função alternativa a
qsort (),
std::sort (). Na verdade,
std::sort vem em duas versões, ambas templates, o que permite que elas sejam específicas para cada tipo de dados (em vez de trabalhar com
void * ), e também com um comparador de elementos que pode ser default (usando o operador
< ) ou de um tipo qualquer, desde que se possa usar um dado
pf desse tipo na forma “
pf(elem1, elem2) ” (sendo
elem1 e
elem2 elementos da faixa que se quer ordenar).
Modificando nosso exemplo acima de C com
qsort () para C++ com
std::sort , poderíamos ficar com algo mais ou menos assim.
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
#include <cstring>
struct registro {
std::string nome;
std::chrono::steady_clock::time_point nascimento;
};
typedef std::vector<registro> catalogo;
/* Funções de comparação retornam true se arg1 vem antes de arg2 */
bool compara_registro_nome(const registro &r1, const registro &r2){
return std::strcoll(r1.nome.c_str(), r2.nome.c_str())<0;
}
bool compara_registro_nascimento(const registro &r1, const registro &r2){
return r1.nascimento<r2.nascimento;
}
void ordena_catalogo_nome(catalogo &cat){
std::sort(cat.begin(), cat.end(), compara_registro_nome);
}
void ordena_catalogo_nascimento(catalogo &cat){
std::sort(cat.begin(), cat.end(), compara_registro_nascimento);
}
Essa versão é mais segura quanto a tipo do que a versão em C. Em particular, não existe ponteiro para
void em lugar nenhum. Aliás, desconsiderando os argumentos usados na chamada a
strcoll (), não existe nenhum ponteiro no código, mas todos foram substituídos por referências, e referências constantes sempre que possível.
Tirando isso, porém, não houve maiores diferenças em relação à versão em C.
Entretanto, suponha que você queira poder trabalhar, ao mesmo tempo, com diferentes locales, para acomodar diferentes idiomas, que têm critérios diferentes para a ordenação de strings.
Obviamente você não seria louco de criar várias versões diferentes de
compara_registro_nome () e
ordena_catalogo_nome (). Adicionar mais um parâmetro a
compara_registro_nome () também não poderia ser feito, pois ela se tornaria incompatível com o uso por
std::sort ().
Felizmente, o C++ permite outra abordagem.
Você deve ter percebido que eu disse acima que uma das versões de
std::sort () aceita receber um terceiro argumento, de um tipo qualquer, desde que um objeto desse tipo aceite ser parte de uma expressão com aspecto de chamada de função. Em C, isso só seria possível se o tipo desse objeto fosse um ponteiro de função. Em C++, um tipo que seja ponteiro de função também atende ao requisito, mas isso também é possível com qualquer tipo composto (classe, estrutura ou
union ) que sobrecarregue o operador
() com parâmetros compatíveis com o que
std::sort () pretender usar.
Imagine a seguinte classe.
#include <locale>
class comparador_localizado_nome {
private:
std::locale locale;
public:
comparador_localizado_nome(): locale() { }
comparador_localizado_nome(const std::string &loc_name): locale(loc_name) { }
bool operator()(const registro &s1, const registro &s2) const {
// Note que nós também usamos “operator()” da classe std::locale
return locale(s1.nome, s2.nome)<0;
}
};
Com a classe acima, nós podemos criar uma segunda versão (que pode inclusive coexistir com a outra, graças à sobrecarga de funções do C++) da nossa função
ordena_catalogo_nome ().
void ordena_catalogo_nome(catalogo &cat, const comparador_localizado_nome &comp_obj){
std::sort(cat.begin(), cat.end(), comp_obj);
}
Melhor ainda do que isso seria escrevê-la numa forma que aceita tanto o uso de objetos-funções quanto de ponteiros para funções.
#include <functional>
void ordena_catalogo_nome(catalogo &cat, std::function<bool(const registro &, const registro &)> comp=comparador_localizado_nome()){
std::sort(cat.begin(), cat.end(), comp);
} (Note que esta última versão torna todas as anteriores desnecessárias.)
O modo de usar essas coisas seria parecido com o seguinte.
int main(void){
catalogo cat;
/* Preenche o catálogo com uma grande quantidade de dados. */
// Cria três cópias do catálogo original.
catalogo cat_de(cat), cat_fr(cat), cat_es(cat);
// Cria objetos do tipo comparador....
comparador_localizado_nome ordena_alemao("de"), ordena_frances("fr");
// ... e ordena usando tais objetos.
ordena_catalogo_nome(cat_de, ordena_alemao);
ordena_catalogo_nome(cat_fr, ordena_frances);
// Ordena usando um objeto temporário.
ordena_catalogo_nome(cat_es, comparador_localizado_nome("es"));
/* ... */
}
Voltando ao primeiro exemplo, ainda em C, você deve ter percebido que foi necessário criar funções de comparação que só são empregadas uma vez no programa, que é dentro das respectivas funções de ordenação. Também a versão em C++ apresentada acima recorreu ou a funções ou classes que tiveram de ser definidas no em nível de namespace. No fim das contas, além de haver funções ou classes usadas poucas vezes, ainda por cima tais funções ou classes eram extremamente simples. Tão simples que, se pudessem ser escritas diretamente no ponto em que são usadas, o programa possivelmente ficaria menor e até mais fácil de entender.
Eis aí algumas razões para usar lambdas.
Expressões lambda, introduzidas no C++11, são uma forma de expressar e definir funções próximas (ou mesmo dentro) do escopo em que serão usadas, e são empregadas principalmente quando uma determinada função esperar recebe funções (ou objetos-funções) como argumentos.
Só para para uma ideia, veja duas formas de conseguir ordenar um dado do tipo
catalogo usando lambdas.
// Ordena por nome (comparação direta de strings)
std::sort(
cat.begin(), cat.end(),
[](const registro &r1, const registro &r2) -> bool { return r1.nome<r2.nome; }
);
// Ordena por hora de nascimento.
std::sort(
cat.begin(), cat.end(),
[](const registro &r1, const registro &r2) -> bool { return r1.nascimento<r2.nascimento; }
);
Note a forma. Os colchetes vazios são uma forma de indicar ponteiro (pode parecer meio estranha, mas que talvez fique menos estranha se você lembrar que, quando usados dentro da declaração dos parâmetros de função, eles têm sentido de ponteiro em C). Logo em seguida vem a lista de parâmetros, entre parênteses, como em qualquer declaração de função. O tipo de retorno é indicado após a declaração dos parâmetros, com a sintaxe “
-> nome_do_tipo ”, que também é aceita para funções comuns. Por fim, vem o corpo da função, que também é idêntico ao de funções comuns.
Lambdas também podem ser atribuídos a variáveis, e usados depois por meio de tais variáveis.
auto comp=[](const registro &r1, const registro &r2) -> bool { return r1.nome<r2.nome; };
std::sort(cat.begin(), cat.end(), comp);
Por serem definidas dentro de um escopo limitado de bloco, o C++ permite que as expressões lambda enxerguem valores ou referências que são visíveis nesse bloco. Isso se faz colocando entre os colchetes os nomes das variáveis a ter o valor copiado, ou a expressão do endereço das variáveis.
#include <iostream>
int main(){
int a=0, b=1;
double c=2.0, d=1.0;
// O lambda recebe cópias dos valores de a e c, e referências para b e d.
// Quando ele for invocado (não durante a definição!), os valores de b e d
// podem ser modificados, e o bloco de main() verá tais modificações. Se
// os valores de a e c não podem ser modificados pelo lambda.
auto lambda=
[a, &b, c, &d]() -> void {
std::cout << "lambda:\t" << a << '\t' << ++b << '\t' << c << '\t' << (d*=1.5) << '\n';
}
;
// Outro lambda com cópias dos valores de a e c, e referências para b e d.
// Quando ele for invocado (não durante a definição!), os valores de b e d
// podem ser modificados, e o bloco de main() verá tais modificações. Como
// este lambda tem o atributo “mutable” as cópias de a e c poderão ser alte-
// radas, porém tais alterações não se refletem no corpo de main(), mas per-
// sistem para o lambda enquanto ele existir.
auto lambda2=
[a, &b, c, &d]() mutable -> void {
std::cout << "lambda:\t" << --a << '\t' << ++b << '\t' << (c/=2) << '\t' << (d*=1.5) << '\n';
}
;
std::cout << "main:\t" << a << '\t' << b << '\t' << c << '\t' << d << '\n';
lambda();
std::cout << "main:\t" << a << '\t' << b << '\t' << c << '\t' << d << '\n';
lambda();
std::cout << "main:\t" << a << '\t' << b << '\t' << c << '\t' << d << '\n';
lambda();
std::cout << "main:\t" << a << '\t' << b << '\t' << c << '\t' << d << '\n';
std::cout << '\n';
std::cout << "main:\t" << a << '\t' << b << '\t' << c << '\t' << d << '\n';
lambda2();
std::cout << "main:\t" << a << '\t' << b << '\t' << c << '\t' << d << '\n';
lambda2();
std::cout << "main:\t" << a << '\t' << b << '\t' << c << '\t' << d << '\n';
lambda2();
std::cout << "main:\t" << a << '\t' << b << '\t' << c << '\t' << d << '\n';
}
Opcionalmente, você pode passar todos os símbolos como cópia para o lambda colocando um sinal
= entre os colchetes. Se, em vez disso, usar o sinal
& , todos os símbolos serão passados por referência.
O que explica o funcionamento dos lambdas é que o compilador converte a expressão lambda numa classe de objeto-função anônima. Todos os objetos copiados ou passados por referência produzem membros de dados internos nessa classe anônima, e um construtor que copia o valor ou endereço das variáveis locais correspondentes para os respectivos campos membros. Esses campos são constantes, a menos que o lambda seja declarado como
mutable . Também é gerada uma função
operator() () com a mesma assinatura do lambda e com o corpo para ele definido.