【C++】C++11新特性重点:可变参数+lambda

C++11新特性第二篇重点

文章目录


前言

上一篇我们重点讲解了右值引用+移动语义,关于移动构造和移动赋值还有一些需要补充的知识:

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

一、较为简单的新特性

1.delete关键字

当我们不想让一个类的拷贝构造函数去拷贝,那么我们可以在后面加上delete关键字,如下:

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& s) = delete;
private:
	string _name;
	int _age;
};
int main()
{
	Person p1;
	Person p2(p1);
	return 0;
}

 我们将Person类中拷贝构造函数加上delete后,我们发现不能再使用拷贝构造了,如下图:

 2.final关键字

final关键字的作用是修饰一个类,让一个类不能被继承。final也可以修饰一个成员函数,可以让这个成员函数不能被重写。

3.override关键字

override可以强制检查派生类的虚函数是否完成重写,如果没有完成重写就报错。(纯虚函数可以强制子类完成重写)

4.可变参数列表

C++11 的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改
进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。
// Args 是一个模板参数包, args 是一个函数形参参数包
// 声明一个参数包 Args...args ,这个参数包中可以包含 0 到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl;
}
int main()
{
	ShowList();
	ShowList(" ", 11, "hello", 12);
	return 0;
}

 上面是一个可变参数包,第一个参数包的参数为0,第二个参数包的参数为4,一个空格字符串,一个数字11,一个hello字符串,一个数字12,我们sizeof打印的时候一定是...在括号外面,实际上...就代表参数包,下面我们看看运行结果是否正确:

 那么如果解析这个可变参数包呢?解析参数包的意思就是拿到参数包的内容,比如第二个参数包的4个参数。这里我们给出两种方法:

第一种:递归方式

void ShowList()
{
	cout << endl;
}
template <class T, class ...Args>
void ShowList(const T& val, Args... args)
{
	/*cout << sizeof...(args) << endl;*/
	cout << val << " ";
	ShowList(args...);
}
int main()
{
	ShowList();
	ShowList(" ", 11, "hello", 12);
	return 0;
}

注意:递归传参数包的时候后面必须加上...

第二个方法:

template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args),0)... };
	cout << endl;
}
int main()
{
	ShowList(1, 'A', "hello", 54);
	return 0;
}

这种展开参数包的方式,不需要通过递归终止函数,是直接在 expand 函数体中展开的 , printarg
不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式
实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。
expand 函数中的逗号表达式: (printarg(args), 0) ,也是按照这个执行顺序,先执行
printarg(args) ,再得到逗号表达式的结果 0 。同时还用到了 C++11 的另外一个特性 —— 初始化列
表,通过初始化列表来初始化一个变长数组 , {(printarg(args), 0)...} 将会展开成 ((printarg(arg1),0),
(printarg(arg2),0), (printarg(arg3),0), etc... ) ,最终会创建一个元素值都为 0 的数组int arr[sizeof...
(Args)] 。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分 printarg(args)
打印出参数,也就是说在构造 int 数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在
数组构造的过程展开参数包

 上面arr数组中存放的参数包至于这个数组多大看参数包有多少个参数,...一定要放在括号外面这样编译器才能推出来参数,其中PrintArg用的逗号表达式是因为最后的结果必须是0,如果我们不用PrintArg的返回值的话,我们可以修改一下代码:

template <class T>
int PrintArg(T t)
{
	cout << t << " ";
	return 0;
}
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { PrintArg(args)... };
	cout << endl;
}
int main()
{
	ShowList(1, 'A', "hello", 54);
	return 0;
}

将...写在括号外面的意思是:每次生成一个PrintArg函数,具体要生成多少个要看这个参数包有多少个参数。

不知道大家还记不记得STL容器中的emplace系列,这个系列就是用参数包做的:

实际上这个参数包就是C语言中的可变参数列表变化而来的。

 下面我们看看参数包对应的emplace系列的使用:

int main()
{
	list<sxy::string> mylist;
	sxy::string s1("hello");
	mylist.push_back(s1);
	mylist.emplace_back(s1);
	return 0;
}

我们先看看emplace系列和普通的push_back有什么不同,可以看到emplace系列用了可变参数包,这里用了万能引用,也就是左值引用和右值引用都可以使用,下面我们运行起来看看区别:

 运行后我们发现并没有什么区别,我们再试试右值:

 那么emplace系列真正的区别点在哪里呢?下面我们看看匿名对象当右值:

int main()
{
	list<sxy::string> mylist;
	//有区别
	mylist.push_back("3333");
	mylist.emplace_back("3333");
	return 0;
}

 我们可以看到,push_back在插入右值的时候,是构造+移动构造,而emplace版本只需要一个构造,为什么会这样呢?因为我们传的const cahr*类型,这个编译器推参数包的时候直接推导为char*类型,而我们string类本来就有char*类型的直接构造,所以emplace比普通版本的插入效率高!

总结:emplace系列对于深拷贝的类效果不大,因为这里的参数包没有多大效果。对于Date类这种emplace就起了效果,参数包可以直接调用其构造函数。

二、lambda表达式

首先我们学习一下语法:

lambda 表达式书写格式:
[capture-list] (parameters) mutable -> return-type { statement }
1. lambda 表达式各部分说明
[capture-list] : 捕捉列表 ,该列表总是出现在 lambda 函数的开始位置, 编译器根据 []
判断接下来的代码是否为 lambda 函数 捕捉列表能够捕捉上下文中的变量供 lambda
函数使用
(parameters) :参数列表。与 普通函数的参数列表一致 ,如果不需要参数传递,则可以
连同 () 一起省略
mutable :默认情况下, lambda 函数总是一个 const 函数, mutable 可以取消其常量
性。使用该修饰符时,参数列表不可省略 ( 即使参数为空 )
->returntype :返回值类型 。用 追踪返回类型形式声明函数的返回值类型 ,没有返回
值时此部分可省略。 返回值类型明确情况下,也可省略,由编译器对返回类型进行推
{statement} :函数体 。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
到的变量。
注意:
lambda 函数定义中, 参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为
。因此 C++11 最简单的 lambda 函数为: []{} ; lambda 函数不能做任何事情。
捕获列表说明
捕捉列表描述了上下文中那些数据可以被 lambda 使用 ,以及 使用的方式传值还是传引用
[var] :表示值传递方式捕捉变量 var
[=] :表示值传递方式捕获所有父作用域中的变量 ( 包括 this)
[&var] :表示引用传递捕捉变量 var
[&] :表示引用传递捕捉所有父作用域中的变量 ( 包括 this)
[this] :表示值传递方式捕捉当前的 this 指针
注意:
a. 父作用域指包含 lambda 函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割
比如: [=, &a, &b] :以引用传递的方式捕捉变量 a b ,值传递方式捕捉其他所有变量
[& a, this] :值传递方式捕捉变量 a this ,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误
比如: [=, a] = 已经以值传递方式捕捉了所有变量,捕捉 a 重复
d. 在块作用域以外的 lambda 函数捕捉列表必须为空
f. lambda 表达式之间不能相互赋值 ,即使看起来类型相同

上面的语法大家先了解一下,下面我们用例子来讲解lambda表达式中有哪些该注意的事项:

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

以前我们对于这种自定义的类进行比较的时候都需要去单独写一个仿函数或者函数指针等,虽然能解决排序的问题但是相对较麻烦,下面我们用lambda表达式比较一下:

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
   3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool
		{
			return g1._price < g2._price;
		});
	for (auto& e : v)
	{
		cout << e._name << " : " << e._evaluate << " : " << e._price << endl;
	}
	cout << "---------------------------------------" << endl;
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool
		{
			return g1._name < g2._name;
		});
	for (auto& e : v)
	{
		cout << e._name << " : " << e._evaluate << " : " << e._price << endl;
	}
	
}

 我们可以看到lambda表达式对于这样的情况处理起来简直太方便了,具体的语法我们下面进行讲解:

 上图中我们可以详细的看到lambda的每个部分,注意函数体内的;和语句结尾的;。就以上面的表达式为例,首先我们的返回值类型是可以省略的,如下图:

 使用起来也很简单,可以用lambda对象,也可以直接用对象名:

 注意:lambda表达式中有两个位置是一定不可以省略的,第一个是捕捉列表[],第二个是函数体{}.

下面我们讲讲捕捉列表的作用:

比如我们现在要实现一个交换的函数:

int main()
{
	int x = 10, y = 20;
	auto swap = [](int x, int y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	swap(x, y);
	cout << x << ":" << y << endl;
	return 0;
}

 这里为什么没有交换成功呢?因为我们lambda表达式中的参数是传值的,函数体内交换的是x和y的临时拷贝,所以我们应该用传引用:

 这次我们看到成功了,下面我们在用用捕捉列表,捕捉列表可以捕捉我们当前作用域的值:

 捕捉后我们发现不能修改,这是因为lambda捕捉列表为了防止有人直接修改作用域的值,所以捕捉过来的值不允许修改,那么如何修改呢?我们需要在后面加上mutable,这个的含义是异变:

int main()
{
	int x = 10, y = 20;
	auto swap = [x, y]()mutable
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	swap();
	cout << x << ":" << y << endl;
	return 0;
}

 我们发现捕捉后还是不能成功交换啊,这是因为默认的捕捉是传值捕捉,我们需要在捕捉变量前面加上&符号,这样就是引用捕捉了:

 下面我们两个特殊的用法:

全部引用捕捉:

全部传值捕捉:

 混合捕捉:

 上面捕捉列表的意思是:x使用传值捕捉,其他全是引用捕捉。

下面我们用一下线程来演示lambda表达式的魅力:(不知道线程相关知识的可以看我linux线程的文章)

#include <thread>

void func(int n)
{
	for (int i = 0; i < n; i++)
	{
		cout << i << " ";
	}
	cout << endl;
}
int main()
{
	thread t1(func, 10);
	thread t2(func, 5);
	t1.join();
	t2.join();
	return 0;
}

以上是传统写法,我们让两个线程去执行func函数打印,下面是结果:

 出现上面结果的原因不难解释,因为我们的线程是并发访问的,所以这个函数是很有可能被两个线程同时进入的,这就导致换行出现问题。下面我们用lambda表达式来写上面的代码:

int main()
{
	int n = 10;
	thread t1([n]()
		{
			for (int i = 0; i < n; i++)
			{
				cout << i << " ";
			}
			cout << endl;
		});
	thread t2([n]()
		{
			for (int i = 0; i < n; i++)
			{
				cout << i << " ";
			}
			cout << endl;
		});
	t1.join();
	t2.join();
	return 0;
}

 以上就是lambda表达式的所有内容了,后面我们讲解c++11线程的时候也会大量的用到lambda表达式。


总结

函数对象和lambda表达式:

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了 operator() 运算符的
类对象。
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;
}

在上述代码中,我们运行起来查看汇编代码看lambda的底层是什么:

 实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如

果定义了一个 lambda 表达式,编译器会自动生成一个类,在该类中重载了 operator() 。如上大家应该明白了,实际上lambda表达式的底层就是用函数对象实现的,就像范围for一样看起来很高大上,实际上底层是迭代器。

猜你喜欢

转载自blog.csdn.net/Sxy_wspsby/article/details/130977015