[C++11] wrapper de expressão lambda


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:
Insira a descrição da imagem aqui
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):
Insira a descrição da imagem aqui

Perceber:

  1. O escopo pai refere-se ao bloco de instruções que contém a função lambda.
  2. 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. .
  3. 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.
  4. As listas de captura de funções Lambda fora do escopo do bloco devem estar vazias.
  5. 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.
  6. 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;
}

Insira a descrição da imagem aquiDo ponto de vista da montagem, não é difícil descobrir que as expressões lambda também são chamadas operatorpara 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 uuidum 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 thisponteiros, 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.
Insira a descrição da imagem aquiMas preste atenção ao seguinte método de chamada:
Insira a descrição da imagem aquitambé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 bindusá -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 placeholdersum 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


Acho que você gosta

Origin blog.csdn.net/m0_68872612/article/details/131031897
Recomendado
Clasificación