当前位置: 首页 > article >正文

C++11新特性:lambda表达式,包装器,新的类功能

1. lambda表达式

1.1 基本语法

lambda表达式本质上是一个匿名函数对象,但是和普通函数不一样他可以定义在函数内部。

lambda表达式使用层而言没有类型,所以我们一般使用auto或者模板参数定义的对象去接收lambda对象

lambda表达式的格式如下:

[ capture-list ] (parameters)-> return type{function body}

  •  [capture-list]:捕捉列表,该列表在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量来给lambda函数使用,详细下面说明,捕捉列表可以为空但是不能省略
  • ( parameters ):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()一起省略
  • ->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确的情况下,也可省略,由编译器对返回类型进行推导。
  • { funct body }:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
#include<iostream>
using namespace std;

int main()
{

	auto fun1 = [](int x1, int x2)-> int {cout << "222" << endl; return x1 + x2; };
	cout << fun1(1, 2) << endl;

    //1.捕捉为空也不能省略
    //2.参数为空可以省略
    //3.返回值为空可以省略,并且可以通过返回对象自动推导
    //4.函数体是不可以省略的
    //
	auto fun2 = []
		{
			cout << "hello world" << endl;
			return 0;
		};
	cout << fun2() << endl;

	int a = 20, b = 10;
	auto swap1 = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap1(a, b);
	cout << a << b<< endl;
	return 0;

}
1.2 捕捉列表

lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要捕捉了。

第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[a,b,&c]表示a和b是值捕捉,c是引用捕捉。会改变c的值

void test2()
{
	//只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3, e = 4;

	auto func1 = [a, b, &c]
		{
			//值捕捉的变量是不可修改的,引用捕捉的变量可以修改。
			//a++;
			c++;
			int sum = a + b + c;
			return sum;
		};
	cout << "c的值为:" << c << endl;
	cout << func1() << endl;

}

第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个= 表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉,这样我们lambda表达式中用了哪些变量,编译器就会自动捕捉那些变量。

void test2()
{
	//只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3, e = 4;

	auto func2 = [=] ()-> int
		{
			//值引用,不可以修改
			//a++;
			int sum = a + b + c;
			return sum;
		};
	cout << func2() << endl;
	auto func3 = [&]
		{
			a++;
			b++;
			c++;
			int sum = a + b + c;
			return sum;
		};
	cout << func3() << endl;
}

第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&a]表示其他变量隐式值捕捉,a引用捕捉;[&,a,b],表示其他变量引用捕捉,a,b值捕捉。当使用混合捕捉时,第一个元素必须是&或者=,并且&混合捕捉时后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉

void test2()
{
	//只能用当前lambda局部域和捕捉的对象和全局对象
	int a = 0, b = 1, c = 2, d = 3, e = 4;

	auto func4 = [=, &a]
		{
			//值捕捉不能修改
			//b++;
			a++;
			int sum = a + b + c;
			return sum;
		};
	cout << func4() << endl;

	auto func5 = [&, a, b]
		{
			//值捕捉不能修改
			//a++;
			c++;
			int sum = a + b + c;

			return sum;
		};
	cout << func5() << endl;
}

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

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

void test3()
{
	int a = 0, b = 1, c = 2;

    //传值捕捉本质是一种拷贝,并且被const修饰了
    //mutable相当于去掉了const属性,可以修改了
    //但是修改了不会影响外面被捕捉的值,因为是一种拷贝
    //
	auto func1 = [=]()mutable
		{
			a++;
			b++;
			c++;
			return a + b + c;
		};
	cout << "a的值为" << a << endl;
	cout << "b的值为" << b << endl;
	cout << "c的值为" << c << endl;
}
1.3 lambda表达式的应用

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

#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;

struct Boy
{
	string _name;
	int _age;
	string _id;
	
	Boy(const string s1, int p, const string s2)
		:_name(s1)
		, _age(p)
		, _id(s2)
	{

	}
};

struct CompareAge
{
	bool operator() (const Boy& b1, const Boy& b2)
	{
		return b1._age < b2._age;
	}
};
struct CompareId
{
	bool operator() (const Boy& b1, const Boy& b2)
	{
		return b1._id < b2._id;
	}
};

int main()
{
	vector<Boy> v = { {"张三",17,"561325"},{"李四",19,"603149"},{"王五",18,"264146"} };
	sort(v.begin(), v.end(), CompareAge());
	for (auto e : v)
	{
		cout << e._age << endl;
	}
	sort(v.begin(), v.end(), CompareId());
	for (auto e : v)
	{
		cout << e._id << endl;
	}
	sort(v.begin(), v.end(), [](const Boy& b1, const Boy& b2) {return b1._age > b2._age; });
	for (auto e : v)
	{
		cout << e._age << endl;
	}
	sort(v.begin(), v.end(), [](const Boy& b1, const Boy& b2) {return b1._id > b2._id; });
	for (auto e : v)
	{
		cout << e._id << endl;
	}

	return 0;

}
1.4 lambda的原理

lambda的原理和范围for类似,编译后从汇编指令层来看,压根就没有lambda和范围for。范围for的底层是迭代器实现,lambda底层是仿函数对象,也就是说我们写了一个lambda以后,编译器会生成一个对应的仿函数的类

仿函数的类型是编译时按照一定规则生成的,保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质上是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传哪些对象。

我们通过汇编看一下

struct Boy
{
	string _name;
	int _age;
	string _id;
	
	Boy(const string s1, int p, const string s2)
		:_name(s1)
		, _age(p)
		, _id(s2)
	{

	}
};

struct CompareAge
{
	bool operator() (const Boy& b1, const Boy& b2)
	{
		return b1._age < b2._age;
	}
};
int main()
{
	vector<Boy> v = { {"张三",17,"561325"},{"李四",19,"603149"},{"王五",18,"264146"} };
	auto func1 = [](const Boy& b1, const Boy& b2) {return b1._id > b2._id; };

	sort(v.begin(), v.end(), CompareAge());

	sort(v.begin(), v.end(),func1 );


	return 0;

}

 在两个操作的最后一步call,都是调用operator(),lambda表达式类型是lambda_1,这个类型名的规则是编译器自己定制的,保证不同的lambda不冲突。

2. 新的类功能

2.1 默认的构造

原来C++类中,有6个默认的成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载,最后重要的是前四个,后两个用处不是很大,默认成员函数就是我们不写编译器会生成一个默认的。C++新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。

当你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

当你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任一个,那么编译器就会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐个成员按字节拷贝,自定义类型成员,则看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(和上面移动构造完全类似)。

2.2 defult和delete

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成默认构造了,我们可以通过default关键字显示指定移动构造生成。

#include<iostream>
#include<algorithm>
using namespace std;

class Person
{
public:
	Person() = default;
	Person(const char* name, int age)
		:_name()
		,_age(age)
	{
	}
	Person(const Person& p) = default;
	Person(Person&& p) = default;

private:
	string _name;
	int _age;
};

int main()
{
	Person p1;
	Person p2=p1;
	Person p3=move(p1);

	return 0;
}

如果想要限制某些默认函数的生成,在C++11中只需要在该函数声明加上=delete即可,该语法让编译器不生成对应函数的默认版本,将=delete修饰的函数为删除函数。

class Person
{
public:

	//Person() = delete;
	Person(Person&& p) = delete;
private:
	string _name;
	int _age;
};
2.3 final与override

之前讲过这里简单介绍一下

final:修饰虚函数,表示虚函数不能再被重写。

override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

3. 包装器

3.1 function

std::function是一个类模板也是一个包装器。std::function的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambda、bind表达式等。所存储的可调用对象被称为std::function的目标。若std::function不含目标,则称它为空。调用空std::function的目标会导致抛异常std::bad_function_call异常。

function的原型如下,被定义在<functional>头文件中。

template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

函数指针、仿函数、lambda等可调用对象的类型各不相同,std::function的优势就是统一类型,对他们都可以包装,这样在很多地方就方便声明可调用对象的类型。

#include<iostream>
#include<functional>
using namespace std;
int add(int a, int b)
{
	return a + b;
}

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};


class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}
private:
	int _n;
};
void Print()
{
	cout << "hello world" << endl;
}

包装各种可调用对象

int main()
{
	function<int(int, int)> f1 = add;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };
	function<void()> f4 = Print;
	f4();

	cout << f1(1, 1)<<endl;
	cout << f2(1, 1)<<endl;
	cout << f3(1, 1)<<endl;

}

包装静态成员函数

成员函数要指定类域明确前面加&才能获取地址

	function<int(int, int)> f5 = &Plus::plusi;
	cout << f5(1, 1) << endl;

    包装不同成员函数
    普通成员函数还有一个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以

	Plus p;
	function<double(Plus*, double, double)> f6 = &Plus::plusd;
	cout << f6(&p, 2.2, 2.2) << endl;

	function<double(Plus, double, double)> f7 = &Plus::plusd;
	cout << f7(p, 2.2, 2.2) << endl;

	function<double(Plus&&, double, double)> f8 = &Plus::plusd;
	cout << f8(move(p), 2.2, 2.2) << endl;
3.2 bind

bind是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。bind可以用来调整参数个数和参数的顺序。

bind也在<functional>这个头文件中。

template <class Fn, class... Args>
 /* unspecified */ bind (Fn&& fn, Args&&... args);
 
with return type (2)
template <class Ret, class Fn, class... Args>
 /* unspecified */ bind (Fn&& fn, Args&&... args);

调用bind的一般形式:auto newCallable = bind(callable,arg_list); 

newCallable本身是一个可调用对象

arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。

 当我们调用newCallable时,newCallable会调用callable,并传给arg_list中的参数。

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

#include<functional>

int sub(int a, int b)
{
	return a - b;
}

int threesub(int a,int b,int c)
{
	return a - b - c;
}

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a - b;
	}
};


class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}
	static int plusi(int a, int b)
	{
		return a - b;
	}
	double plusd(double a, double b)
	{
		return (a - b) * _n;
	}
private:
	int _n;
};

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int main()
{
    return 0;
}

bind来改变参数顺序

	auto b1 = bind(sub, _1, _2);
	cout << b1(3, 1) << endl;
	//bind本质上返回一个仿函数对象
	// 调整参数顺序0
	// _1代表第一个实参
	// _2代表第二个实参
	auto b2 = bind(sub, _2, _1);
	cout << b2(3, 1) << endl;

调整参数个数

	auto b3 = bind(sub, 100, _1);
	cout << b3(10) << endl;

	auto b4 = bind(sub, _1, 100);
	cout << b4(10) << endl;

	auto b5 = bind(threesub, _1, _2, 100);
	cout << b5(5, 1) << endl;

	auto b6 = bind(threesub,100, _1, _2);
	cout << b6(5, 1) << endl;

	auto b7 = bind(threesub, _1, 100, _2);
	cout << b7(5, 1) << endl;

还可以对成员函数对象进行绑定,就不需要每次都传递参数了

	function<double(Plus&&, double, double)> ff1 = &Plus::plusd;
	Plus p;
	cout << ff1(move(p), 1.1, 1.1) << endl;
	cout << ff1(Plus(), 1.1, 1.1) << endl;

	function<double(double, double)> ff2 = bind(&Plus::plusd, Plus(), _1, _2);
	cout << ff2(1.1, 1.1) << endl;

实践中设计一个计算利率的lambda表达式,也可以绑定一些参数来进行计算,不同年利率,不同存储年份。

	auto func1 = [](double rate, double money, int year) {
		double ret = money;
		for (int i = 0; i < year; i++)
		{
			ret += ret * rate;
		}
		return ret - money;
		};
	// 绑死⼀些参数,实现出⽀持不同年华利率,不同⾦额和不同年份计算出复利的结算利息 
	function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
	function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
	function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
	function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);
	cout << func3_1_5(1000000) << endl;
	cout << func5_1_5(1000000) << endl;
	cout << func10_2_5(1000000) << endl;
	cout << func20_3_5(1000000) << endl;

这篇就到这里啦(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤

ヾ( ̄▽ ̄)Bye~Bye~


http://www.kler.cn/a/395784.html

相关文章:

  • ROS进阶:使用URDF和Xacro构建差速轮式机器人模型
  • 深入探索 React Hooks:原理、用法与性能优化全解
  • A3超级计算机虚拟机,为大型语言模型LLM和AIGC提供强大算力支持
  • sqlsever 分布式存储查询
  • WebLogic 介绍
  • 逐行加载 HTML 内容并实时显示效果:使用 wxPython 的实现
  • 富格林:正确应付阻挠虚假交易
  • 如何用Java爬虫“采集”商品订单详情的编程旅程
  • C++中 ,new int(10),new int(),new int[10],new int[10]()
  • 除了网页标题,还能用爬虫抓取哪些信息?
  • 实时数据流的革命:分布式数据库的挑战与实践
  • 图像处理之cornerdetection(角点检测)综述
  • 从cityengine到unreal,游戏引擎助力城市设计
  • Linux服务器下连接kingbase并执行SQL的脚本
  • 关于指针对象的问题
  • 搜维尔科技:TOUCH力反馈设备睿尔曼机械臂,遥操作机械臂
  • 实现 Toy-React , 实现 JSX 渲染
  • 通过css的哪些方式可以实现隐藏页面上的元素?
  • spark的学习-05
  • Java中的集合类与线程安全的讨论
  • ETLCloud支持的数据处理类型包括哪些?
  • ubuntu docker里面安装Omniverse Launcher不能登陆
  • 【Elasticsearch】01-ES安装
  • node对接ChatGpt的流式输出的配置
  • Apache Doris:深度优化与最佳实践
  • Dev C++ 无法使用to_string方法的解决