C++11实用的新特性:lambda表达式与包装器function与bind

目录


一、lambda表达式

1.1 概念

C++11 引入的 Lambda 表达式是一种就地定义的匿名函数对象 。它允许你在需要函数的地方直接写一段逻辑,而不必提前定义一个命名函数或手写一个完整的仿函数类。

在学习lambda表达式之前,我们的使用的可调用对象只有函数指针仿函数对象 ,函数指针的类型定义起来比较麻烦,仿函数要定义一个类(重载()),相对会比较麻烦。使用lambda去定义可调用对象,既简单又方便。

与普通函数不同,lambda表达式可以在函数内部实现。
语法格式:

cpp 复制代码
[捕捉列表](参数)-> return type {函数体}

举个简单的例子:

cpp 复制代码
auto f = [](int a, int b) -> int { return a + b; };
cout << f(2, 3) << endl;

说明:

  • 开头的 [ ]必须要写,因为编译器根据[ ]来判别其为lambda表达式;
  • ()即为参数列表,如果不需要参数,()可以省略;
  • -> return type:返回值类型,也可以省略,编译器会自动推导;
  • { }即函数体,与普通函数一样。

1.2 捕捉列表

顾名思义,就是将函数体中会用到的变量,对象等进行抓取,使得其在函数体内部可用。

cpp 复制代码
void test()
{
	int a = 2, b = 3;
	auto Add = [a, b]() {return a + b; };
	cout << Add() << endl; // 调用
}

那么捕捉有什么讲究呢?具体有四种捕捉方式:

  1. 值捕捉
    我们在上面直接将变量名或对象在捕捉列表捕捉,就是值捕捉。
  2. 引用捕捉
    引用捕捉就是在变量前加 &
cpp 复制代码
void test()
{
	int a = 2, b = 3;
	auto Add = [&a, &b]() {return a + b; };
	cout << Add() << endl; // 调用
}
  1. 显示捕捉

    我们在捕捉列表,直接写明要捕捉的变量或对象就是显示捕捉。显示捕捉时既可以值捕捉,也可以引用捕捉,同样也可以在一个捕捉列表中同时出现,值捕捉和引用捕捉。

  2. 隐式捕捉

    隐式捕捉有隐式值捕捉,即直接在捕捉列表中写一个 =;隐式引用捕捉,即在捕捉列表直接写一个 &。
    需要注意:

在用到隐式捕捉时,= 或 & 必须写在捕捉列表最前面,两者不能同时出现。如果此时想混合捕捉,且前面已经是 =,那么后面必须全部是引用捕捉;同理,如果前面已经是 &,那么后面必须全部是值捕捉。

cpp 复制代码
void test()
{
	int a = 2, b = 3, c = 4;
	auto Add1 = [=]() {return a + b + c; }; // 隐式值捕捉
	auto Add2 = [&]() {return a + b + c; }; // 隐式引用捕捉
	// 混合捕捉
	auto Add3 = [=, &b]() {return a + b + c; }; 
	auto Add4 = [&, c]() {return a + b + c; }; 
	// 错误示范:隐式值捕捉,后面又显示值捕捉(重复)
	// auto Add2 = [=, b, c]() {return a + b + c; }; 
}

细节补充:

默认情况下,值捕捉是被const修饰的,也就是说值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
引用捕捉的对象可以修改

cpp 复制代码
int main()
{
	int a = 1, b = 1, c = 1, d = 1;
	auto f = [a, b, &c, &d]() {
		// a++; // 报错
		// b++; // 报错
		c++;
		d++;
	};
	f(); // 调用
	cout << "c = " << c << ", d = " << d << endl;
	return 0;
}
cpp 复制代码
int main()
{
	int a = 1, b = 1, c = 1, d = 1;
	auto f = [a, b, &c, &d]() mutable {
		a++; 
		b++; 
		return a + b;
	};
	cout << f() << endl; // 调用
	// 并不改变实参
	cout << "a = " << a << ", b = " << b << endl;
	return 0;
}

对于全局变量和静态局部变量不需要捕捉,lambda表达式中可以直接使用。这也意味着lambda表达式如果定义在全局位置,捕捉列表必须为空。

1.3 lambda的应用

对于需要自定义排序的类对象,我们不再需要重载operator(),而是直接用lambda表达式。

cpp 复制代码
#include <vector>
#include <algorithm>
#include <string>
struct Product 
{
	std::string _name;
	double _price;
	int _stock;

	Product(std::string name, double price, int stock)
		:_name(name),
		_price(price),
		_stock(stock)
	{ }
};
struct Compare
{
	bool operator()(const Product& p1, const Product& p2)
	{
		return p1._price < p2._price;
	}
};
int main()
{
	std::vector<Product> products = {
	{"iPhone", 5999.0, 50},
	{"MacBook", 12999.0, 20},
	{"AirPods", 1299.0, 200}
	};
	// 重载operator()
	std::sort(products.begin(), products.end(), Compare());

	// lambda表达式:按价格降序
	std::sort(products.begin(), products.end(),
		[](const Product& a, const Product& b) {
			return a._price > b._price;
		});
	return 0;
}

1.4 lambda原理

其实就是编译器在底层将我们写的lambda表达式转换成了一个仿函数,实际上并没有所谓的lambda表达式。

cpp 复制代码
auto f = [x, &y](int a) -> int { return x + a + y; };
// 上面的 Lambda,编译器会翻译成类似下面的类:
cpp 复制代码
class __lambda_unique_id {          // 编译器生成的唯一类名
    int x;                          // 值捕获的成员(拷贝)
    int& y;                         // 引用捕获的成员(引用)
public:
    __lambda_unique_id(int _x, int& _y) : x(_x), y(_y) {}
    
    int operator()(int a) const {   // 函数调用运算符
        return x + a + y;
    }
};

// 实际调用时:
// auto f = __lambda_unique_id(x, y);
// f(5);  // 等价于 f.operator()(5);

二、包装器

2.1 function

什么是function,就是将具有相同类型返回值,参数列表的可调用对象进行包装,让可调用对象对外呈现一种类型,其头文件为 <functional>。可调用对象有4个:函数指针仿函数lambdabind,bind是什么我们下面介绍!!!

四种可调用对象,四种完全不同的类型:

cpp 复制代码
int plain_func(int, int) { return 0; }            // 函数指针类型:int(*)(int,int)
struct Functor { int operator()(int, int) {} };   // 仿函数类型:Functor
auto lambda = [](int, int) { return 0; };         // lambda类型:编译器生成的匿名类
auto bound = std::bind(plain_func, _1, _2);       // bind类型:std::_Bind<...>

std::function可以包装存储这些对象,这些对象成为function的目标。若std::function不含目标,则称它为空。调用空std::function的目标就会导致抛出std::bad_function_call异常。
function的用法:

cpp 复制代码
std::function<返回类型(参数类型列表)> f;

所以对于上面的四个可调用对象,就可以进行统一包装:

cpp 复制代码
std::function<int(int,int)> f1 = plain_func;   // ✅
std::function<int(int,int)> f2 = Functor();    // ✅
std::function<int(int,int)> f3 = lambda;       // ✅
std::function<int(int,int)> f4 = bound;        // ✅

应用场景:

用function对象调用成员函数:

痛点:由于成员函数第一个参数默认为this指针,当我们将成员函数直接赋值给function对象就会报错。

解决:lambda捕捉this。

cpp 复制代码
class A
{
public:
	int Add(int a, int b)
	{
		return a + b;
	}
};

int main()
{
	A a;
	// 想要调用成员函数
	// func_t func = a.Add(2, 3); // 报错
	// 解决:
	std::function<int(int, int)> func = [&a](int x, int y) {
		return a.Add(x, y); };

	cout << func(2, 3) << endl;
	return 0;
}

2.2 bind

std::bind也是一个可调用对象的包装器,相当于函数适配器,其头文件也是 <functional>。有什么特点呢?

它解决的核心问题:固定部分参数,留下部分参数延迟传入。
bind用法:

cpp 复制代码
auto newCall = bind(Call, arg_list);
// 如果想要调用类A中的成员函数Call
auto newCall = bind(&A::Call, arg_list);
// 注意:格式必须是&类名::成员函数名

其中newCall本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的Call的参数。当我们调用newCall时,newCall会调用Call,并传给它arg_list中的参数。

为什么说bind支持固定参数呢?

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是占位符,表示newCall的参数,它们占据了传递给newCall的参数的位置。数值n表示生成的可调用对象中参数的位置:_1为Call的第一个参数,_2为第二个参数,以此类推。_1/_2/_3...这些占位符放到placeholders的一个命名空间中。

cpp 复制代码
int Sub(int x, int y) { return (x - y) * 10; }

void test01()
{
	auto f = bind(Sub, _1, _2);
	cout << f(2, 3) << endl;
}
void test02()
{
	auto f = bind(Sub, _2, _1); // 调整参数顺序
	cout << f(2, 3) << endl;
}

常用场景一:调整参数个数(绑定个别参数)

此时,对于一些固定不变的参数,就不需要再传了!!!

cpp 复制代码
void test03()
{
	auto f1 = bind(Sub, 5, _1);
	cout << f1(3) << endl;

	auto f2 = bind(Sub, _1, 5);
	cout << f2(3) << endl;
}

常用场景二:成员函数调用

cpp 复制代码
struct A
{
	int Sub(int a, int b) { return (a - b) * 10; }
};

void Test()
{
	A a;
	auto f1 = bind(&A::Sub, &a, _1, _2);
	// function包装
	std::function<int(int, int)>  f2 = bind(&A::Sub, &a, _1, _2);
	cout << f1(2, 3) << endl;
	cout << f2(5, 2) << endl;
}

std::bind 绑定成员函数时,把对象指针/引用作为第一个参数传入,就填上了 this 的位置,生成的可调用对象不再需要外部提供 this,编译器视角是 *返回类型(类名, 参数列表)**,同样可以解决我们调用成员函数时this指针的问题。

这些特性在未来写代码的过程中其实非常常见,也很实用,希望能够帮助到大家,上面其实就有我所遇到的场景,如果再有什么实用的场景我也会及时地补充。

相关推荐
Shadow(⊙o⊙)3 小时前
Linux内核级文件系统分析——文件系统入门内核级文章!
linux·运维·服务器·开发语言·c++
cjhbachelor3 小时前
C/C++内存管理
c语言·开发语言·c++
噜噜大王_3 小时前
C++ 类和对象(中):默认成员函数全解
开发语言·c++
Non-existent9875 小时前
海拔批量查询 + 批量 KML 生成工具-WPS 插件 TableGIS 新功能
javascript·c++·excel·wps
咩咦12 小时前
C++学习笔记28:静态成员应用:不用循环求1到n的和
c++·学习笔记·类和对象·static·构造函数·oj·静态成员
EllinY12 小时前
CF2217E Definitely Larger 题解
c++·笔记·算法·构造
筠筠喵呜喵13 小时前
Linux软件开发性能优化
linux·c++·性能优化
Bruce_kaizy13 小时前
c++ linux环境编程——文件io介绍以及open 、write 、read 三剑客深度详解
linux·服务器·c++·ubuntu·操作系统·文件io
PAK向日葵15 小时前
我用 C++ 写了一个轻量级 Python 虚拟机,刚刚开源
c++·python·开源