【C++进阶】C++11(下)可变参数模板&lambda表达式&包装器


我们紧接着上一节的讲解来进行

一,可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。

下面来看一下可变参数的模板函数:


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

Args是一个模板参数包,args是一个函数形参参数包
声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

注:语法不支持使用args[i]这样方式获取可变参数

所以我们用下面的方式来解析参数包:
靠编译器时递归推演来解析参数包


void _Show() {
	cout << endl;
}

template<class T,class ...Args>
void _Show(T& val, Args ...args) {
	cout << val << " ";
	_Show(args...);
}

template<class ...Args>
void Show(Args... args) {
	
	_Show(args...);
}

int main() {

	Show(1, 2, 3,4);
	return 0;
}

在_Show(T& val, Args …args) 这里会解析val为第一个参数,args是剩下的参数构成新的参数包,再继续递归解析。直到最后一个参数时,调用最上面的函数来结束。


这里用逗号表达式也可以来展开参数包
,逗号表达式就是用逗号隔开的多个表达式。从左向右依次执行。整个表达式的结果是最后一个表达式的结果

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);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, PrintArg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。因为逗号表达式会按顺序执行逗号前面的表达式。

在(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数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

二,lambda表达式

然后我们来讲解lambda表达式。
我们在之前讲过仿函数,如果想要对一个自定义的元素进行排序,可以使用std的sort方法
但是需要传入不同的类(仿函数)来控制默认的排序顺序。对于编程来说还是比较麻烦的

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

所以C++11引入了lambda表达式
格式是:

[](int x)->int {  //...  };

如果用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){
		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; });
}

这样我们就可以做到按照我们想要的方式去排序,而且不用写很多的类实现仿函数了


现在我们依次来介绍一下lambda表达式:
在这里插入图片描述
lambda表达式其实相当于于不用自己写的仿函数,其底层也是仿函数

下面来看看用法:
用法1:
遇到一个lambda表达式,底层会生成一个类,调用时像仿函数一样


auto f1 = [](int x) { cout << x << endl; return 0; };
f1(2);

用法2:

int x = 1, y = 2;
cout << x << " " << y << endl;

auto f2 = [x, y]() mutable
{
	int tmp = x;
	x = y;
	y = tmp;
};

f2();
cout << x << " " << y << endl;

注意:这里在lambda表达式后面要加上mutable,这是因为默认生成的类中成员变量不可修改
这里是一个传值捕捉,在[]捕捉列表中捕捉这个域内的x,y,其实是拷贝一下,然后去初始化自己的成员x,y。

但是输出后我们看到这里的x ,y没有发生交换:
在这里插入图片描述

这是因为捕捉列表捕捉这个域内的x,y是拷贝去初始化自己的成员x,y 但是没有真正的改变这个域中的x,y

所以要进行引用捕捉,也就是拿外面变量(这两个变量本身)去初始化lambda表达式自己的成员变量。

auto f3 = [&x, &y]()//可以不加mutable
{
	int tmp = x;
	x = y;
	y = tmp;

};
f3();
cout << x << " " << y << endl;

现在就可以交换了。
在这里插入图片描述

这里再补充一下:

也可以 [=]进行捕捉,意思是传值捕捉这个域中的全部
[&] 表示引用捕捉这个域的全部
[=,&a,&b] 表示还可以搭配使用,传值捕捉全部,但是其中的a,b是引用捕捉

三,包装器

现在来说包装器

function包装器 也叫作适配器。包装器实际上是一个类模板,包含了可调用对象(函数指针,仿函数,lambda表达式)

为什么需要包装器呢?

这是因为对于函数指针,比较反人类设计就劝退一大部分的C/C++玩家了,然后仿函数用的时候比较麻烦,要写一个重载()的类,而lambda表达式又取不到类型

来看看包装器的用法:
使用时要加上头文件: < functional >
格式是:

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

Ret : 被调用函数的返回类型
Args… :被调用函数的形参

下面分别是三种可调用对象,用包装器来使用:

//函数
int f(int a, int b) {
	return a + b;
}

//仿函数
struct func {
	int operator()(int a, int b) {
		return a + b;
	}
};

//lambda表达式
auto lambda = [](int a, int b) { return a + b; };


int main() {
	function<int(int, int)> fun1 = f;//函数指针
	cout << fun1(1, 2) << endl;
	
	function<int(int, int)> fun2 = func();//仿函数
	cout << fun2(1, 2) << endl;

	function<int(int, int)> fun3 = lambda;//lambda表达式
	cout << fun3(1, 2) << endl;

	return 0;
}

还可以对类成员函数进行包装:
这里有一个包含static修饰的静态成员函数,和一个普通成员函数

struct Plus {
	static int plusi(int a, int b) {
		return a + b;
	}

	double plusd(double a, double b) {
		return a + b;
	}
};


对于包装类成员函数需要 :
1.加指定的类域 (Plus:: )
2.加&取地址(静态成员函数可以不加)

int main() {
	function<int(int, int)> fun4 = &Plus::plusi;//对于静态成员
	cout<<fun4(1, 2)<<endl;

对于非静态类成员函数要加this指针:

	function<double(Plus*, double, double)> fun6 = &Plus::plusd;
	Plus ps;
	cout << fun6(&ps, 1.1, 2.2) << endl;

这里还有个特殊处理:也可以传入一个Plus()匿名对象

	
	function<double(Plus, double, double)> fun5 = &Plus::plusd;
	cout << fun5(Plus(), 1.1, 2.3) << endl;//

	return 0;
}

这里还有一个很有意思的用法,大家可以自己看一下,如果有疑问欢迎大家来提问

int main() {
	map<string, function<int(int , int )>> cmdOp =
	{
		{"函数指针",f},
		{"仿函数",func()},
		{"lambda表达式",lambda}

	};

	auto ret = cmdOp["函数指针"](1, 2);
	cout << ret << endl;

	cmdOp["仿函数"](1, 2);
	cmdOp["lambda表达式"](1, 2);


	return 0;
}

四,绑定bind

bind包装器实际上是一个函数模板,只不过用来调整可调用对象参数的顺序和个数

我们直接来看用法:

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


正常使用包装器时:

int main() {
	//正常使用
	function<int(int, int)> f1 = Sub;
	cout << f1(10, 5) << endl;

如果我们想要调整参数顺序,那么就可以:

	function<int(int, int)> f2 = bind(Sub, placeholders::_2,placeholders::_1);
	cout << f2(10, 5) << endl;

其中的 placeholders::_2placeholders::_1 分别代表下面 f2 函数中传入的第一个参数和第二个参数
在这里插入图片描述
也可以调整参数个数:只要传一个参数即可

	function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
	cout << f3(5) << endl;//

	return 0;
}

在这里插入图片描述

五,其他新特性

我们在前面C++入门时介绍过auto,可以进行自动类型的推导
但是如果我们拿到一个auto的类型,想要知道其具体类型呢?
比如想拿auto的ret去定义一个vector

int main() {
	auto i = 1;
	int j = 2;
	auto ret = i + j;
	vector<>

	return 0;
}

我们是不可以用auto去定义的
我们可以打印来看一下auto的类型:

cout << typeid(ret).name() << endl;

但是总不可能每次要知道的时候就打印一下吧,所以这里有一个关键字decltype可以推导类型并且可以使用

int main() {
	auto i = 1;
	int j = 2;
	auto ret = i + j;
	cout << typeid(ret).name() << endl;
	vector<decltype(ret)> v;

	return 0;
}

这样我们就可以去正常使用了。

六,总结

我们关于C++11的讲解就先到这里,当然还有很多新特性没有说到,像智能指针还有一些线程相关的,我们在后面再进行介绍。这里只是介绍了一些主要的更新。如果大家感兴趣可以自己去探索。

相关推荐

  1. C++11lambda表达式 & 包装

    2024-04-11 15:50:04       13 阅读
  2. C++--C++11包装

    2024-04-11 15:50:04       28 阅读
  3. C++11lambda表达式及function包装

    2024-04-11 15:50:04       6 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-11 15:50:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-11 15:50:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-11 15:50:04       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-11 15:50:04       20 阅读

热门阅读

  1. Python的魔法书:揭秘编程的基本咒语

    2024-04-11 15:50:04       13 阅读
  2. starrocks的fe节点启动不起来的解决办法

    2024-04-11 15:50:04       16 阅读
  3. 蓝桥杯练习题 —— 十六进制转八进制(python)

    2024-04-11 15:50:04       13 阅读
  4. 【如何应用OpenCV对图像进行二值化】

    2024-04-11 15:50:04       14 阅读
  5. SpringBoot整合RabbitMQ

    2024-04-11 15:50:04       14 阅读
  6. 计算机专业考研考哪些专业课

    2024-04-11 15:50:04       14 阅读
  7. 《策略模式(极简c++)》

    2024-04-11 15:50:04       14 阅读
  8. 选择成为一名程序员的原因

    2024-04-11 15:50:04       13 阅读
  9. 2017NOIP普及组真题 2. 图书管理员

    2024-04-11 15:50:04       16 阅读
  10. tcp/ip细节

    2024-04-11 15:50:04       11 阅读