C++11新特性——一篇文章带你了解透

前言

一、列表初始化

1.1 C++98的{}

1.2 C++11的{}

1.3 C++11的initializer_list

二、可变模版参数

2.1 基本语法及原理

2.2 包扩展

2.3 emplace系列接口

三、类的新功能

3.1 默认的移动构造和移动赋值

3.2 成员变量声明时给缺省值

3.3 default和delete

3.4 final和override

四、STL中的一些变化

五、lambda

5.1 lambda表达式语法

5.2 捕捉列表

5.3 lambda的应用

5.4 lambda的原理

六、包装器

6.1 function

6.2 bind

总结


前言

前一篇的文章中讲了右值引用以及移动语义,本篇文章就继续来讲解 C++11 的其他新特性,至于发展历史这些在上一篇已经提过了,本篇就不再做详细说明。


一、列表初始化

1.1 C++98的{}

struct A
{
	int _x;
	int _y;
};

int main()
{
	int arr1[] = { 1, 2, 3, 4, 5 };
	int arr2[5] = { 0 };
	A p = { 1, 2 };

	return 0;
}

1.2 C++11的{}

C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化。
内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化
了以后变成直接构造。
{}初始化的过程中,可以省略掉=。
C++11列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便
利,如容器push/insert多参数构造的对象时,{}初始化会很方便,因为多参数的构造函数也支持隐式类型转换。
struct A
{
	int _x;
	int _y;
};

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	// 内置类型支持
	int x1 = { 2 };
	// ⾃定义类型支持
	// 这⾥本质是用{2025, 1, 1}构造⼀个Date临时对象
	// 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{2025, 1, 1}直接构造初始化d1
	// 运⾏⼀下,我们可以验证上面的理论,发现是没调⽤拷贝构造的
	Date d1 = { 2025, 1, 1 };
	// 这⾥d2引⽤的是{2025, 1, 1}构造的临时对象
	const Date& d2 = { 2025, 1, 1 };
	// C++98⽀持单参数时类型转换,也可以不⽤{}
	Date d3 = { 2025 };
	Date d4 = 2025;
	// 可以省略掉=
	A p1{ 1, 2 };
	int x2{ 2 };
	Date d6{ 2024, 7, 25 };
	const Date& d7{ 2024, 7, 25 };

	vector<Date> v;
	v.push_back(d1);
	v.push_back(Date(2025, 1, 1));
	// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐
	v.push_back({ 2025, 1, 1 });

	return 0;
}

一般是用在传参中比较多,非常方便,直接走类型转换,不需要定义有名对象或者匿名对象


1.3 C++11的initializer_list

比如说我们在使用vector/list的时候,它们的构造函数都有一个用n个val去构造,但是这个构造出来的容器内所有的值都是一样的,没法做到我想用1,2,3,4,5来初始化,就只能先定义出来对象再一个一个的插入,那 C++11库中提出了一个 initializer_list 的类

这个类的本质是底层开一个数组,将数据拷贝过来,initializer_list内部有两个指针分别指向数组的开始和结束。initializer_list支持迭代器遍历。

容器支持一个initializer_list的构造函数,也就支持任意多个值构成的 {x1,x2,x3...} 进行初始化。STL中的容器支持任意多个值构成的 {x1,x2,x3...} 进行 初始化,就是通过initializer_list的构造函数支持的。
int main()
{
	auto ili = { 1, 2, 3, 4, 5 };
	//initializer_list<int> ili = { 10, 20, 30 };
	
	//成员变量有2个指针,32位下大小是8
	cout << sizeof(ili) << endl;

	//打印得到的类型是class std::initializer_list<int>
	cout << typeid(ili).name() << endl;

	//地址非常接近,验证了我们说的是在栈上开一块数据,把数据拷贝过来
	int x = 0;
	cout << &x << endl;
	cout << ili.begin() << endl;
	cout << ili.end() << endl;

	//下面两种写法看似一样,其实并不一样,一个是调构造,参数的个数有要求,
	// 另一个是initializer_list,传多少个都可以
	Date d1 = { 2025,1,1 };
	vector<int> v = { 1,2,3 };

	//initializer_list也支持赋值版本
	v = { 10,20,30 };

	return 0;
}

二、可变模版参数

2.1 基本语法及原理

C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称
为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函
数参数。
// 普通模版
template <class ...Args> 
void Func(Args... args) 
{}

// 左值引用模版
template <class ...Args> 
void Func(Args&... args) 
{}

// 万能引用模版
template <class ...Args> 
void Func(Args&&... args) 
{}
我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或
typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出
接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟普通模板一样,每个参数实例化时遵循引用折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
template <class ...Args>
void Print(Args&&... args)
{}

int main()
{
	double d = 2.2;
	Print(); // 包里有0个参数
	Print(0); // 包里有1个参数
	Print(0, string("111")); // 包里有2个参数
	Print(0, string("111"), d); // 包里有3个参数

	return 0;
}
void Print();

template <class T1>
void Print(T1&& arg1);

template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);

template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
如果没有可变参数模板,我们实现出这样的多个函数模板才能支持这里的功能,他是类型泛化基础上叠加数量变化,让我们泛型编程更灵活,那可变模版参数就节省了我们的很多力气,如果这里最多可能有10个参数,那我们还要写10个函数模版加上一个普通函数,那也太费事了,现在我们一个可变参数模版就解决了。而且不管我们写一个可变参数模版还是写很多个函数模版,编译器都会实例化出这么多类(如下)。就把我们的工作交给编译器去做了,所以可变参数模版的意义还是很大的。
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

大家都知道sizeof操作符是干嘛的,就是计算所占内存空间的大小,在这里要引入一个sizeof...操作符,它的功能是去计算参数包中的个数。


template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;
}

那既然我们可以通过这个运算符求出参数包中的个数,那可不可以遍历参数包中的个数次,来依次拿到里面的内容,解析出来呢?

template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;

	for (int i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << " ";
	}
	cout << endl;
}

一定要注意是没有这种语法的,解析参数包有其他的方法但是这种方法不可以,不可以用[]访问。下面就来介绍解析参数包的方法。


2.2 包扩展

对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个
包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元
素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。
void ShowList()
{
	// 编译时递归的终止条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}

template<class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
};

template <class ...Args>
void Print(Args... args)
{
	ShowList(args...);
}

int main()
{
	double x = 2.2;
	Print(); // 包⾥有0个参数
	Print(1); // 包⾥有1个参数
	Print(1, string("xxxxx")); // 包⾥有2个参数
	Print(1, string("xxxxx"), x); // 包⾥有3个参数

	return 0;
}

参数是0个时,直接匹配到普通的ShowList,参数是一个时,x就是参数包的第一个,剩下0个参数的参数包传给普通的ShowList,参数是两个时,x是参数包的第一个,剩下1个参数的参数包继续递归调自己,x是参数包的第一个,剩下0个参数的参数包传给普通的ShowList,参数是三个时,x是参数包的第一个,剩下2个参数的参数包继续递归调自己,x是参数包的第一个,剩下1个参数的参数包继续递归调自己,x是参数包的第一个,剩下0个参数的参数包传给普通的ShowList,,就依次解析出了所有的参数。在这里注意,是编译时递归展开,不是运行时,其次是虽然说是递归,但是每一次因为参数会变化,所以每一次函数模版都会实例化成不同的函数,那每一次调用的也就都是不同的函数,所以应该说是调自己的重载版本。


初次之外,C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。 

template <class T>
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}
template <class ...Args>
void Arguments(Args... args)
{
}

template <class ...Args>
void Print(Args... args)
{
	Arguments(GetArg(args)...);
	cout << endl;
}

int main()
{
	double x = 2.2;
	Print(); // 包⾥有0个参数
	Print(1); // 包⾥有1个参数
	Print(1, string("xxxxx"));
	Print(1, string("xxxxx"), x);

	return 0;
}

把参数包的每个参数都传给getArg处理,处理后的返回值作为实参组合参数包传给Arguments,本质可以理解为编译器编译时,包的扩展模式,这种方法是比较抽象的。

但是在实际中,我们基本很少会把参数包的内容解析出来,下面我们来看看具体应用。


2.3 emplace系列接口

这里会涉及右值引用和移动语义以及完美转发,建议如果大家不是很熟悉的话先去看看上一篇文章,链接在这里:C++ 带你彻底了解右值引用和移动语义-CSDN博客文章浏览阅读253次,点赞18次,收藏8次。右值引用以及移动语义,是 C++11 中的一个很重要的知识,增加了移动构造和移动赋值,因为是直接转移资源,所以效率远比拷贝的代价低。C++11中还有很多新的特性,由于篇幅原因我们下一篇文章再介绍。!! https://blog.csdn.net/2401_84771520/article/details/147386164?spm=1001.2014.3001.5501而且也将继续用到自己实现的string类和list类,为了省略篇幅就不在这里再贴一份了,大家需要看的话也可以到上一篇中去看。

C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上
兼容push和insert系列,emplace,emplace_front,emplace_back,和push/insert是对应的关系, 我们将从插入一个有名对象、move以后的左值、匿名对象、以及隐式类型转换四组值来讨论emplace和push的区别。
int main()
{
	list<hx::string> lt;
	cout << "*********************************" << endl;

	hx::string s1("111111111111");
	lt.emplace_back(s1);
	lt.push_back(s1);
	cout << "*********************************" << endl;

	lt.emplace_back(move(s1));
	lt.push_back(move(s1));
	cout << "*********************************" << endl;
	
	lt.emplace_back(hx::string("11111111111"));
	lt.push_back(hx::string("11111111111"));
	cout << "*********************************" << endl;

	lt.emplace_back("111111111111");
	lt.push_back("111111111111");
	cout << "*********************************" << endl;

	return 0;
}

通过运行结果,可以看到的是,前三组的结果都是一样的, 第一组都是构造+拷贝构造,第二组都是移动构造,第三组都是构造+移动构造,但唯独第四组不同,emplace_back是构造,push_back是构造+移动构造,这是为什么呢?

这是因为,对于push_back来说,list实例化了,value_type就确定了,现在value_type就是string,const char*没法直接传给string,需要先构造一个临时对象,然后到结点上的时候,走移动构造转移资源。而对于emplace_back来说,list实例化了跟我没有关系,我是等到实参传给形参的时候推演类型,那传上来的是const char*,就推出来了const char*,然后构造string的参数包一路向下传,等到结点上时,直接用const char*去构造string。所以一个是直接构造,一个是构造+移动构造。

那再来看一组例子

int main()
{
	list<pair<hx::string, int>> lt1;
	cout << "*********************************" << endl;

	pair<hx::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	lt1.push_back(kv);
	cout << "*********************************" << endl;

	lt1.emplace_back(move(kv));
	lt1.push_back(move(kv));
	cout << "*********************************" << endl;

	lt1.emplace_back(pair<hx::string, int>("111111", 1));
	lt1.push_back(pair<hx::string, int>("111111", 1));
	cout << "*********************************" << endl;

    // 注意这里的写法
	lt1.emplace_back("苹果", 1);
	lt1.push_back({ "苹果", 1 });
	cout << "*********************************" << endl;

	return 0;
}

可以看到的是,存pair和存string的结果是一样的,只有第四组不同,而且这里的写法要注意一下,push_back必须要加{},emplace_back不能加{},那为什么第四组的结果不同呢?

这是因为,对于push_back来说,list实例化了,value_type就确定了,现在value_type就是pair<string, int>,形参只有一个参数,那也只能传一个参数,多参数构造函数的隐式类型转换必须加上{},所以push_back必须加{},先构造一个临时对象,然后到结点上的时候,走移动构造转移资源。而对于emplace_back来说,list实例化了跟我没有关系,我是等到实参传给形参的时候推演类型,那传上来的是const char*, int,就推出来了const char*, int,对于{}编译器会识别成initializer_list,但是initializer_list里面的值类型又是不一样的,编译器不能确定这到底是什么,所以emplace_back不能加{},然后构造pair<string, int>的参数包一路向下传,等到结点上时,直接用const char*, int去构造pair<string, int>。所以一个是直接构造,一个是构造+移动构造。

对于深拷贝的类来说,直接构造和构造+移动构造的区别不大,这里的区别反而在于浅拷贝,浅拷贝的类移动构造没有可以转移的资源,还是只能老老实实拷贝,这时和直接构造的效率就有区别了,所以综合来说emplace_back更加高效,推荐使用emplace系列替代insert和push系列。

接下来我们用我们自己的list来实现一下emplace系列

之前是push_back传给insert,现在是emplace_back传给emplace,由于参数包中的参数我们并不知道是什么,可能是左值也可能是右值,所以参数包往下一层传的时候需要完美转发,维持自身的属性。

template<class ...Args>
void emplace_back(Args&&... args)
{
	emplace(end(), forward<Args>(args)...);
}

template<class ...Args>
void emplace(iterator pos, Args&&... args)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(forward<Args>(args)...);

	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	++_size;
}

template<class ...Args>
list_node(Args&&... args)
	: _prev(nullptr)
	, _next(nullptr)
	, _val(forward<Args>(args)...)
{}

三、类的新功能

3.1 默认的移动构造和移动赋值

这个在上一篇文章的结尾也已经提过了,移动构造和移动赋值是新增加的两个默认成员函数,如果没有显示实现移动构造、析构、拷贝构造、拷贝赋值中的任意一个,那编译器会生成一个默认的移动构造,默认的移动构造对于内置类型进行浅拷贝,对于自定义类型会去调它的移动构造,如果它没有移动构造,就调它的拷贝构造。

如果没有显示实现移动构造、析构、拷贝构造、拷贝赋值中的任意一个,那编译器会生成一个默认的移动赋值,默认的移动赋值对于内置类型进行浅拷贝,对于自定义类型会去调它的移动构赋值,如果它没有移动赋值,就调它的拷贝赋值。


3.2 成员变量声明时给缺省值

成员变量声明时给缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个缺省值初始化。


3.3 default和delete

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因
这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用
default关键字显示指定移动构造生成。
class Person
{
public:
	Person(const char* name = "1111", int age = 20)
		:_name(name)
		, _age(age)
	{}

	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}

	// 指定生成
	Person(Person&& p) = default;

private:
	hx::string _name;
	int _age;
};
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明就可以,
这样如果想要调用就会报错。而在C++11中,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
	Person(const char* name = "1111", int age = 20)
		:_name(name)
		, _age(age)
	{}

	// 指定不生成
	Person(Person&& p) = delete;

private:
	hx::string _name;
	int _age;
};

3.4 final和override

final修饰一个类,该类不能被继承。final修饰虚函数,该虚函数不能被重写。

override是检查派生类是否重写了父类的某个虚函数的,如果没重写就会报错。


四、STL中的一些变化

在C++11更新的新容器有array,forward_list,unordered_set,unordered_map,其中最有用的是unordered_set,unordered_map,另外两个在实际中使用的很少。

STL中容器的新接口也不少,最重要的就是右值引用和移动语义相关的push/insert/emplace系列
接口和移动构造和移动赋值,如果没有移动构造/移动赋值,那插入右值只能是构造+拷贝构造,有了移动构造/移动赋值,插入右值就是构造+移动构造,有了emplace系列之后,插入某些右值,就是直接构造,不管是深拷贝的类还是浅拷贝的类,emplace系列的效率都要更高。
增加了initializer_list版本的构造,容器的初始化更方便了。
增加了cbegin/cend等迭代器的相关接口,但是由于begin/end已经提供了const版本,所以cbegin/cend接口的意义不大。
范围for遍历,写起来很方便,底层就是替换成迭代器。
还有auto自动识别类型,如果命名空间没有展开,取map中的迭代器的话那要写一长串,但是现在一个auto就解决了。

五、lambda

5.1 lambda表达式语法

lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使用层而言没有类型,所以一般使用auto或者模板参数定义的对象去接收 lambda 对象。
lambda表达式的格式: [capture-list] (parameters)-> return type { function boby }
[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来
判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供 lambda 函数使
用,捕捉列表可以传值和传引用捕捉,捕捉列表为空也不能省略。
(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连
同()一起省略。
->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此
部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以
使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
int main()
{
	auto add = [](int x, int y)->int {return x + y; };
	cout << add(1, 2) << endl;

	// 省略了参数和返回值
	auto func = []
		{
			cout << "hello world" << endl;
			return 0;
		};
	func();

	int a = 0, b = 1;
	// 省略了返回值
	auto swap = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap(a, b);

	return 0;
}

5.2 捕捉列表

如果我不想传参,并且还想在lambda内用外层作用域的变量,这时就要通过捕捉列表进行捕捉,捕捉的方式有两种,一种是值捕捉,一种是引用捕捉。

int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [a, b]
		{
			// 值捕捉的变量不能修改
		    // a++;
			// b++;
			int ret = a + b;
			return ret;
		};
	cout << func1() << endl;

    return 0;
}

显示的值捕捉了a和b

需要注意的是,值捕捉是外面的拷贝,而且默认是带有const属性的,不可以修改。

int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	auto func1 = [&a, &b]
		{
			// 引用捕捉的变量可以修改
		    a++;
			b++;
			int ret = a + b;
			return ret;
		};
	cout << func1() << endl;

    return 0;
}

显示的引用捕捉了a和b

引用捕捉可以修改,而且在lambda内修改了后外面也就修改了。


如果这个时候外面的变量很多,假如有10个变量,在lambda内都要用,这时候捕捉10个变量很麻烦,所以可以用隐式的捕捉方式

int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	auto func2 = [=]
		{
			int ret = a + b + c;
			return ret;
		};
	cout << func2() << endl;

    return 0;
}
隐式的值捕捉了a和b
在捕捉列表写⼀个=表示隐式值捕捉,这样我们在lambda内用了哪些 变量,编译器就会自动捕捉那些变量。但是值捕捉依然是外面的拷贝,且默认带有const属性,无法修改。
int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	auto func3 = [&]
	{
		a++;
		c++;
		d++;
	};
    func3();

    return 0;
}

隐式的引用捕捉了a和b

在捕捉列表写⼀个&表示隐式引用捕捉,这样我们在lambda内用了哪些变量,编译器就会自动捕捉那些变量。


还可以在捕捉列表中混合使用显示捕捉和隐式捕捉

int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	auto func4 = [&, a, b]
	{
		//a++;
		//b++;
		c++;
		d++;
		return a + b + c + d;
	};
	func4();

    return 0;
}

混合捕捉

a和b值捕捉,其他变量引用捕捉,所以c、d可以变,但是a、b不能变,混合捕捉的时候,隐式的一定要放在前面。

int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	auto func5 = [=, &a, &b]
	{
		a++;
		b++;
		//c++;
		//d++;
		return a + b + c + d;
	};
    func5();

    return 0;
}

混合捕捉

a和b引用捕捉,其他变量值捕捉,所以c、d不能变,但是a、b可以变,混合捕捉的时候,隐式的一定要放在前面。


需要注意:捕捉列表不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,直接用就可以
int x;

int main()
{
	int a = 0, b = 1, c = 2, d = 3;
	static int m = 0;
	auto func6 = []
		{
			int ret = x + m;
			return ret;
		};

    return 0;
}
如果lambda表达式如果定义在全局位置,那么捕捉列表必须为空
int x;

auto func = []
	{
		x++;
	};

我们前面说传值捕捉的过来的对象不能修改,默认是带有const属性的,这时加一个mutable在参数列表的后面可以取消其常性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
int main()
{
    int a = 0, b = 1, c = 2, d = 3;
	auto func7 = [=]()mutable
	{
		a++;
		b++;
		c++;
		d++;
		return a + b + c + d;
	};
    cout << func7() << endl;
    cout << a << " " << b << " " << c << " " << d << endl;

    return 0;
}

5.3 lambda的应用

struct Goods
{
	string _name; // 名字
	double _price; // 价格
	int _evaluate; // 评价

	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

int main()
{
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3}, { "菠萝", 1.5, 4 } };
    
    return 0;
}

在实际中我们直接对整型排序的情况是非常少的,大多数是对比如这里的价格,评价排序,就是对这种类里面的成员函数进行排序,我们就要自己控制sort的逻辑,需要传一个可调用对象上去,但是因为函数指针的类型定义起来太费劲,所以在C++中更喜欢用仿函数来替代函数指针。

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;
}

仿函数肯定是能达到要求的,但是我们现在还是只有价格、评价,如果成员变量更多呢,那我们就需要写很多个类,就非常重,而且我们这里还是命名规范,比较的是什么、升序降序都写的很明确,有人命名可能就写一个Compare1, Compare2,那看起来就很费劲,这时候仿函数就不是很好用了,我们就可以用lambda,直接定义局部的函数。

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());

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price < g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price > g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate < g2._evaluate; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._evaluate > g2._evaluate; });

	return 0;
}

由上面的例子大家可以看出,在这种场景下,lambda是非常好用的。


5.4 lambda的原理

lambda的原理和范围for很像,编译后从汇编指令层的角度看,根本就没有 lambda 和范围for
这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个lambda 以后,编译器会生成一个对应的仿函数的类。
lambda的类名是编译按一定规则成成的,保证不同的 lambda 生成的类名不同,lambda参数/返
回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成
的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕
捉,编译器要看使用哪些就传那些对象。
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;

	auto r2 = [rate](double money, int year)->double {
		return money * rate * year;
		};

	Rate r1(rate);
	r1(10000, 2);
	r2(10000, 2);
	
	return 0;
}

lambda的捕捉列表是rate,仿函数类的成员变量是rate,lambda的参数列表和仿函数类的operator()的参数列表一样都是money和year,lambda的参数列表和仿函数类的operator()的返回值一样都是double,lambda的参数列表和仿函数类的operator()的函数体一样,都是money*rate*year

通过汇编我们验证了,捕捉列表的rate,可以看到作为lambda_1类构造函数的参数传递了,这样要拿去初始化成员变量,r2这个lambda对象调用本质还是调用operator(),类型是lambda_1,这个类型名的规则是编译器自己定制的,保证不同的lambda不冲突,而且编译器的规则也不同,所以我们语法层是拿不到这个类型的。


六、包装器

6.1 function

template <class T>
class function; // undefined

template <class Ret, class... Args>
class function<Ret(Args...)>;
function 是一个类模板,也是一个包装器。 function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda bind 表达式等,存储的可调用对象被称为 function 目标。若function 不含目标,则称它为空。调用空 function 的目标导致抛出 std::bad_function_call 异常。
他被定义<functional>头文件中。函数指针、仿函数、 lambda 等可调用对象的类型各不相同, function 的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型,比如我们想把它们都存在一个vector里,那因为类型不统一就没有办法,lambda都拿不到它的类型,这时用一个包装器就可以解决,下面来看几个示例。
int f(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;
};
int main()
{
	// 包装各种可调用对象
	function<int(int, int)> f1 = f;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };

    // 用vector存起来很方便
	vector<function<int(int, int)>> v = { f1, f2, f3 };
	
	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;

    return 0;
}

包装成员函数时要注意指定类域,其次就是静态成员函数前面可以加上&,也可以不加,但是非静态成员函数必须要加

int main()
{
    // 包装静态成员函数
    // 成员函数要指定类域,静态的可以不加&,非静态必须加
    function<int(int, int)> f4 = &Plus::plusi;
    cout << f4(1, 1) << endl;

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

    return 0;
}

但是在这里我们会发现,f5会报错,这其实是因为,非静态的成员函数是有this指针的,所以包装的时候以及在调用的时候都要显示的写出来

int main()
{
    // 包装静态成员函数
    // 成员函数要指定类域,静态的可以不加&,非静态必须加
    function<int(int, int)> f4 = &Plus::plusi;
    cout << f4(1, 1) << endl;
    
    // 包装非静态成员函数
    function<int(Plus*, int, int)> f5 = &Plus::plusd;
    Plus ps;
    cout << f5(&ps, 1, 1) << endl;


    return 0;
}

 除了这样写,还有其他的方式,传对象上去也是可以的

int main()
{
    function<int(Plus, int, int)> f6 = &Plus::plusd;
    cout << f6(ps, 1, 1) << endl;

    function<int(Plus&, int, int)> f7 = &Plus::plusd;
    cout << f7(ps, 1, 1) << endl;

    function<int(Plus&&, int, int)> f8 = &Plus::plusd;
    cout << f8(move(ps), 1, 1) << endl; 

     function<int(Plus&&, int, int)> f9 = &Plus::plusd;
     cout << f9(Plus(), 1, 1) << endl; 
   

    return 0;
}

下面我们来看一下包装器在实际中的具体应用,我们用一道题:逆波兰表达式来介绍

逆波兰表达式的求解方法是:遇见操作数就入栈,遇见操作符就取两个栈顶元素,计算完的结果重新放回栈中。

我们可以用map来映射string和function,遇见+的可调用对象是什么,遇见-的可调用对象是什么    

class Solution {
public:
    int evalRPN(vector<string>& tokens) 
    {
        stack<int> st;
        map<string, function<int(int, int)>> cal = {
            {"+", [](int x, int y) {return x + y;}},
            {"-", [](int x, int y) {return x - y;}},
            {"*", [](int x, int y) {return x * y;}},
            {"/", [](int x, int y) {return x / y;}}
        };

        for(auto str : tokens)
        {
            // 操作符
            if(cal.count(str))
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop();

                // cal[str]拿到可调用对象,再传参调用
                int ret = cal[str](left, right);
                st.push(ret);
            }
            // 操作数
            else
                st.push(stoi(str));
        }

        return st.top();
    }
};

这样的好处是不管有多少个运算符,直接增加映射关系就好,写起来很方便。


6.2 bind

template <class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);

template <class Ret, class Fn, class... Args>
/* unspecified */ bind(Fn&& fn, Args&&... args);
bind 是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器,对接收
的fn可调用对象进行处理后返回一个可调用对象。 bind 可以用来调整参数个数和参数顺序。
bind 也在<functional>这个头文件中。
调用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的命名空间中。
bind可以交换参数的顺序,下面我们看一个示例:
int Sub(int a, int b)
{
	return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
	return (a - b - c) * 10;
}

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};
int main()
{
	auto sub1 = bind(Sub, placeholders::_1, placeholders::_2);
	cout << sub1(10, 5) << endl;

    return 0;
}

这里的调用逻辑是:_1传给a,_2传给b,无论顺序,就算是_2在前,_1在后也是一样,_2传给a,_1传给b,这里的规则就是在第一个就传给第一个形参,在第二个就传给第二个形参,不管是_几在前。但是传参的时候是按照顺序的,第一个实参传给_1/,第二个实参传给_2,10传给_1,5传给_2。

int main()
{
    auto sub2 = bind(Sub, placeholders::_2, placeholders::_1);
    cout << sub2(10, 5) << endl;

    return 0;
}

_2传给a,_1传给b,但是传参的时候是按照_1/_2传的,10传给_1,5传给_2,这样不就达到了交换参数顺序的目的吗


但是在实际中,bind通常用来绑死部分参数,还以上面的Sub函数为例,假如想控制成100-b,就想只传一个a,或者就想是a-100,就想只传一个把,那这时只需要在bind内写死就可以

int main()
{
    // 绑死第一个参数
    auto sub3 = bind(Sub, 100, placeholders::_1);
    cout << sub3(5) << endl;

    // 绑死第二个参数
    auto sub4 = bind(Sub, placeholders::_1, 100);
    cout << sub4(5) << endl;

    return 0;
}

 其余的参数依然遵循上面我们说的传参的规则

int main()
{
    // 绑死第一个参数
    auto sub5 = bind(SubX, 100, placeholders::_1, placeholders::_2);
    cout << sub5(5, 1) << endl;
    
    // 绑死第二个参数
    auto sub6 = bind(SubX, placeholders::_1, 100, placeholders::_2);
    cout << sub6(5, 1) << endl;

    // 绑死第三个参数
    auto sub7 = bind(SubX, placeholders::_1, placeholders::_2, 100);
    cout << sub7(5, 1) << endl;

    return 0;
}

调用SubX函数,分别绑死第一二三个参数

绑定成员函数时也需要指定类域并且取地址,现在我们可以直接把第一个参数,成员函数对象可以直接绑死,这样就不需要每次再显示传了

int main()
{
    function<double(double, double)> f8 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
    cout << f8(1.1, 1.1) << endl;

    return 0;
}

总结

本篇文章讲解了 C++11 的新特性,用起来非常很方便,比如可变模版参数,就是把我们的任务交给编译器了,lambda的发明,以及包装器统一可调用对象的类型,都很值得我们去了解,那 C++11 还有一个智能指针,但是由于智能指针占据的篇幅会有点长,所以我们下一篇文章再讲智能指针,如果大家觉得小编写的不错,可以给一个三连表示感谢,谢谢大家!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值