Diretório de artigos
1 expressão lambda
1.1 Referências
No C++ 98, se quiser classificar os elementos em uma coleção de dados, você pode usar o método std::sort:
#include <algorithm>
#include <functional>
int main()
{
int array[] = {
4,1,8,5,3,7,0,9,2,6 };
// 默认按照小于比较,排出来结果是升序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}
Se os elementos a serem ordenados forem de tipo customizado, o usuário precisará definir as regras de comparação para ordenação:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{
}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = {
{
"苹果", 2.1, 5 }, {
"香蕉", 3, 4 }, {
"橙子", 2.2,
3 }, {
"菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
return 0;
}
Se a nomenclatura do functor for mais padronizada, como o método de nomenclatura acima, tudo bem. Se você encontrar um método de nomenclatura como cmp1 cmp2 cmp3... e não houver comentários, pode ser irritante e você terá que encontrar o correspondente código-fonte para implementá-lo. e se houver muitos códigos em um projeto, o custo da pesquisa será relativamente alto, então o C++ 11 introduziu uma nova sintaxe chamada expressão lambda .
1.2 Sintaxe básica de expressões lambda
Formato de escrita de expressão lambda:
[capture-list] (parameters) mutable -> return-type { statement }
Descrição de cada parte da expressão lambda:
- [lista de captura]: lista de captura. Esta lista sempre aparece no início da função lambda. O compilador usa [] para determinar se o código a seguir é uma função lambda. A lista de captura pode capturar variáveis no contexto para uso pela função lambda.
- (parâmetros):lista de parâmetros. Consistente com a lista de parâmetros de uma função comum, se a passagem de parâmetros não for necessária, ela pode ser omitida junto com ().
- mutável: por padrão, uma função lambda é sempre uma função const e mutável pode cancelar sua constância. Ao usar este modificador, a lista de parâmetros não pode ser omitida (mesmo que o parâmetro esteja vazio).
- -> tipo de retorno:Tipo de valor de retorno. Use o formulário de tipo de retorno de rastreamento para declarar o tipo de valor de retorno da função.Esta parte pode ser omitida se não houver valor de retorno. Se o tipo de valor de retorno for claro, ele também poderá ser omitido e o compilador deduzirá o tipo de retorno.
- {declaração}: Corpo de função. Dentro do corpo da função, além de seus parâmetros, estão disponíveis todas as variáveis capturadas.
Perceber:
Na definição da função lambda, a lista de parâmetros e o tipo de valor de retorno são partes opcionais , enquanto a lista de captura e o corpo da função não podem ser omitidos e podem estar vazios . Portanto, a função lambda mais simples em C++ 11 é:[]{}; Esta função lambda não pode fazer nada.
A lista de captura descreve quais dados no contexto podem ser usados pelo lambda e se eles são passados por valor ou por referência.
- [ var ]: Indica que o método de transferência de valor captura a variável var
- [ = ]: Indica que o método de passagem de valor captura todas as variáveis no escopo pai (incluindo esta)
- [ &var ]: Indica que a variável de captura var é passada por referência
- [ & ]: Indica que a transferência de referência captura todas as variáveis no escopo pai (incluindo esta)
- [ this ]: Indica que o método de transferência de valor captura o ponteiro this atual
Podemos implementar uma adição simples para verificar:
int main()
{
int x, y;
cin >> x >> y;
auto add = [=]()
{
return x + y;
};
cout << add() << endl;
return 0;
}
Na verdade, uma expressão lambda pode ser entendida como uma função sem nome, que não pode ser chamada diretamente. Se quiser chamá-la diretamente, você pode usar auto para atribuí-la a uma variável. Como no add acima, você pode até escrever:cout<< [=](){return x + y;}()<< endl;
Podemos dar uma olhada nos cenários de aplicação mutáveis, como o seguinte código:
int main()
{
int x = 10,y = 20;
auto swapInt = [=] {
int tmp = x; x = y; y = tmp; };
swapInt();
return 0;
}
Quando compilarmos, um erro será reportado diretamente:
por quê? Porque capturamos a variável usando captura de valor, e a variável capturada é uma cópia, e não pode ser modificada por você por padrão (pode ser entendido como adicionar um atributo const), então quando você modifica a variável, ela irá seja diretamente Um erro é relatado, e se quisermos modificá-lo? podemos usarmutável(significando mutável):
Perceber:
- O escopo pai refere-se ao bloco de instruções que contém a função lambda.
- Sintaticamente, uma lista de captura pode consistir em vários itens de captura, separados por vírgulas. Por exemplo: [=, &a, &b]: captura as variáveis a e b por referência e captura todas as outras variáveis por valor. [&, a, this]: captura as variáveis a e this por valor e captura outras variáveis por referência. .
- As listas de captura não permitem que variáveis sejam passadas repetidamente, caso contrário causará erros de compilação. Por exemplo: [=, a]: = capturou todas as variáveis por transferência de valor e captura a repetição de a.
- As listas de captura de funções Lambda fora do escopo do bloco devem estar vazias.
- Uma função lambda num âmbito de bloco só pode capturar variáveis locais no âmbito dos pais. A captura de quaisquer variáveis fora do âmbito ou não locais resultará num erro de compilação.
- As expressões lambda não podem ser atribuídas umas às outras, mesmo que pareçam ser do mesmo tipo.
As notas anteriores são de fácil compreensão, podemos verificar a última nota:
void (*PF)();
int main()
{
auto f1 = []{
cout << "hello world" << endl; };
auto f2 = []{
cout << "hello world" << endl; };
//f1 = f2; // 编译失败--->提示找不到operator=()
// 允许使用一个lambda表达式拷贝构造一个新的副本
auto f3(f2);
f3();
// 可以将lambda表达式赋值给相同类型的函数指针
PF = f2;
PF();
return 0;
}
Nota: Existem comentários no código.
Quanto ao motivo pelo qual a atribuição não é permitida, explicaremos mais tarde, quando explicarmos os princípios das expressões lambda.
1.3 Os princípios subjacentes das expressões lambda
Objeto de função, também conhecido como functor, é um objeto que pode ser usado como uma função.É um objeto de classe que sobrecarrega o operador operador() na classe.
Vamos escrever um trecho de código para verificá-lo:
class Rate
{
public:
Rate(double rate) : _rate(rate)
{
}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lamber
auto r2 = [=](double monty, int year)->double {
return monty * rate * year;};
r2(10000, 2);
return 0;
}
Do ponto de vista da montagem, não é difícil descobrir que as expressões lambda também são chamadas operator
para implementação no nível inferior.Então, por que as expressões lambda não podem atribuir valores umas às outras? A essência é que a nomenclatura subjacente das expressões lambda usa uuid
um método para gerar nomes de classes exclusivos, de modo que objetos de tipos diferentes não podem receber valores naturalmente.
Na verdade, o compilador subjacente trata expressões lambda exatamente como objetos de função. Ou seja,
se uma expressão lambda for definida, o compilador gerará automaticamente uma classe na qual o operador estará sobrecarregado. ().
Em seguida, teste todos: quantos bytes tem o tamanho de um objeto lambda?
A resposta é realmente óbvia. Como a camada inferior da expressão lambda é implementada usando um functor, e o functor é uma classe (classe vazia) sem membro embutido variáveis, o tamanho é 1 byte. Você está correto?
2 embalagens
Os wrappers de função também são chamados de adaptadores. A função em C++ é essencialmente um modelo de classe e um wrapper.
Antes de usar o wrapper, precisamos apresentar o arquivo de cabeçalho. #include <functional>
O protótipo do modelo de classe é o seguinte:
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
Então, como usamos wrappers todos os dias?
// 使用方法如下:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b)
{
return a + b; };
cout << func3(1, 2) << endl;
return 0;
}
Podemos usar um wrapper para aceitá-lo 函数指针 仿函数 lambda
, para que possamos usar um tipo unificado para aceitar diferentes parâmetros para atingir o objetivo de instanciar apenas uma cópia.
No entanto, ao chamar funções-membro não estáticas (excluindo functores) em uma classe, atenção extra deve ser dada ao formato da sintaxe da função:
Por exemplo, como segue:
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 类的成员函数
std::function<int(int, int)> func4 = &Plus::plusi;
cout << func4(1, 2) << endl;
std::function<double(Plus, double, double)> func5 = &Plus::plusd;
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
Sabemos que funções-membro estáticas não incluem this
ponteiros, então não há problema em usar a sintaxe anterior, mas como as funções-membro possuem esses ponteiros, temos queDê um objeto de parâmetro extra(Geralmente gostamos de chamar objetos anônimos) e chamar as funções-membro internas por meio do objeto de parâmetro. E ao especificar o domínio da classe, deve-se adicioná-lo &
, o que é um requisito rígido da gramática.
Mas preste atenção ao seguinte método de chamada:
também podemos usar ponteiros de objeto para chamar, mas neste momento não podemos usar objetos anônimos, porque objetos anônimos são rvalues, o que não é possível &
, mas em circunstâncias normais não escolheremos este caminho .
3 ligar
A função std::bind é definida no arquivo de cabeçalho e é um modelo de função. É como um wrapper de função (adaptador), aceitando um objeto que pode ser chamado e gerando um novo objeto que pode ser chamado para "adaptar" o objeto original.
De modo geral, usamos bind nas duas situações a seguir:
- 1️⃣Alterar a ordem dos parâmetros
- 2️⃣Altere o número de parâmetros
Alterar a ordem dos parâmetros raramente é usado, mas alterar o número de parâmetros é muito interessante. Vamos examiná-los um por um:
Por exemplo, o seguinte programa:
void Print(int x, int y)
{
cout << x << ":" << y << endl;
}
Supondo que não alteremos a implementação da função Print, mas sim troquemos a ordem dos parâmetros na hora de imprimir os resultados, o que podemos fazer?
Podemos bind
usá -lo para lidar com:
int main()
{
int x = 10, y = 20;
Print(x, y);
auto RPrint = bind(Print, placeholders::_2, placeholders::_1);
RPrint(x, y);
return 0;
}
_1 _2
O que diabos está aqui ? Este é na verdade placeholders
um espaço reservado encapsulado no namespace. Como entendemos diretamente, _1 _2... representa o primeiro parâmetro e o segundo parâmetro... respectivamente, pensamos que as posições dos parâmetros a serem trocados podem ser trocados diretamente trocando a ordem dos espaços reservados.
O uso de troca da ordem dos parâmetros é na verdade bastante inútil. Geralmente não usamos isso com muita frequência, mas acho que o cenário de alteração do número de parâmetros é bastante interessante. Vamos dar uma olhada nesta situação:
void mul(double x, double y)
{
cout<< x * y<<endl;
}
struct fun
{
fun(double rate)
:_rate(rate)
{
}
void mulR(double x, double y)
{
cout << x * y * _rate << endl;
}
double _rate;
};
int main()
{
int x = 10, y = 20;
function<void(double, double)> f1 = mul;
function<void(double, double)> f2 = [=](double x,double y) {
cout<< x * y<<endl; };
return 0;
}
Quando exigimos o mesmo formato dos parâmetros acima para aceitar mulR em diversão, se o escrevermos diretamente, um erro será relatado diretamente. Explicamos o princípio em detalhes quando explicamos a função acima, portanto não entraremos em detalhes aqui. Então podemos lidar com isso através do bind:
function<void(double, double)> f3 = bind(&fun::mulR,f, placeholders::_1, placeholders::_2);
Podemos fazer o processamento de vinculação da maneira acima, codificando a primeira vinculação de parâmetro, e então só podemos usar um wrapper de dois parâmetros para aceitá-lo. Não é maravilhoso? Claro, não podemos apenas vincular o primeiro parâmetro, mas também podemos vincular os segundos três n parâmetros por meio de bind. O pequeno detalhe digno de nota é que não importa qual parâmetro vinculamos, nossos outrosOs parâmetros não consolidados só podem continuar a aumentar a partir de _1。