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

C++11中引入的比较常用的新特性讲解(下)

目录

7、新的类功能

8、可变参数模板

8.1、emplace系列的接口和普通的接口的差别

9、lambda表达式

9.1、lambda表达式语法

10、包装器

10.1、function包装器

10.2、bind(绑定)


7、新的类功能

C++11的新东西:移动构造和移动赋值

注:如果你没有自己去实现这两个函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载这三个函数(三个函数通常都是绑定在一起的,要写其中一个,其他两个也需要写),则编译器会自动生成这两个函数。如果你实现了上述的那三个函数中的任意一个,则编译器则不会自动生成移动构造和移动赋值。如果你提供了移动构造或者移动赋值,编译器就不会自动提供拷贝构造和拷贝赋值。

注:默认生成的移动构造/赋值函数,对于内置类型成员会执行逐成员按字节拷贝(值拷贝),对于自定义类型成员,则需要看这个成员是否实现移动构造/赋值,如果实现了就调用移动构造/赋值,没有实现就调用拷贝构造/赋值。

注:编译器是可以同时生成默认的拷贝构造函数和移动构造函数的。前提是你没有显示定义 析构、拷贝构造、拷贝赋值重载、移动构造、移动赋值函数 中的任意一个。

强制生成默认函数的关键字default。

ex:

Person(Person&& p) = default; // 强制生成一个Person类的移动构造

禁止生成默认函数的关键字delete。

ex:

Person(const Person& p) = delete; // 禁止编译器默认生成一个Person类的拷贝构造

注:如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上 = delete即可,该语法指示编译器不生成对应函数的默认版本,称 = delete修饰的函数为删除函数。

注:delete适合于当要求一个类不希望被拷贝的场景。-- ex:库里的 istream 和 ostream,你用传值传参会报错,因为它内部把拷贝构造给 delete 掉了,不希望流对象被拷贝。

举个栗子:假设要求一个类只能在堆上生成对象。

class HeapOnly
{
public:
	static HeapOnly *CreateObj() // 专门写一个函数用于只在堆上生成该类的对象。
								 // 不加static的话,需要通过创建这个类的对象才能去调用这个函数,但是构造函数是私有的,又没法直接创建对象,需要去调用这个函数,所以...
	{
		return new HeapOnly;
	}

private:
	HeapOnly() // 方法一:把这个类的构造函数弄成私有的,这样就没法随便去生成这个类的对象了
	{
	}

	HeapOnly(const HeapOnly &);

	int _a = 1;
};

void test8()
{
	HeapOnly *p1 = HeapOnly::CreateObj(); // 静态成员函数可以通过类域直接调用
	// 但是方法一有漏洞,如果没有把拷贝构造也私有化,那么下面这句代码就能成功运行,然后就成功创建出该类的对象了。
    // HeapOnly obj(*p1); // obj是在栈上创建的 
	// 此时,C++98的做法是把拷贝构造也私有化,即 私有 + 只声明不实现。
	//	     C++11的做法是加了个关键字delete。
}

注:C++11中还新添加了继承和多态中的final与override关键字。

8、可变参数模板

早在C语言中就有可变参数这个概念。

例如Printf函数,去官网上看该函数的文档时会发现它的参数中有一个参数是 ...

C语言中就是用 ... 来代表可变参数的,其底层大概就是用一个数组去存了各种各样的参数,当你传过来一个参数的时候,它内部会自动去这个数组中匹配看是哪种参数。

举个C++中的可变参数模板的栗子:

下面就是一个基本可变参数的函数模板:

Args是一个模板参数包,args是一个函数形参参数包

声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数(类型)。

template <class... Args>
void Example(Args... args) // args:模板参数包Args 定义的 形参对象包
						   // 注意:在<>里 ... 是在Args前面的,但是在()里 ... 是在Args后面的,两个地方刚好相反,这里的格式要记一下。
{
}

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

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数。

这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

奇招一:递归函数方式展开函数包(编译时,参数推导的递归)

举个栗子:

众所周知,C语言的printf有个致命的缺陷就是,只能打印内置类型,对于自定义类型的打印是没有办法。

那现在来实现一个C++版本的可打印任意类型对象的含可变参数的函数。

// 正确的递归出口
void _Cpp_Printf() // 写个空函数就好
{
	cout << endl;
}

// 递归函数
template <class T, class... Args>
void _Cpp_Printf(const T &val, Args... args)
{
	// if (sizeof(...args) == 0) return; // 注意:这样写递归出口是不行的,因为这个递归函数走的是编译时的逻辑,但是这里的sizeof走的是运行时,不匹配。

	cout << val << endl;

	_Cpp_Printf(args...);
}

// 展开函数
template <class... Args>
void Cpp_Printf(Args... args)
{
	// 补充:这个args是可以sizeof一下的,通过sizeof可以知道这个args的参数个数
	cout << sizeof...(args) << endl;
	// 但是:这个args的sizeof的语法设计的很奇怪

	// 补充:不支持这样获取参数包中的每个参数
	// error C3520: “args”: 必须在此上下文中扩展参数包
	/*for (size_t i = 0; i < sizeof...(args); ++i)
	{
		cout << args[i] << endl;
	}
	cout << endl;*/

	// 开始递归
	_Cpp_Printf(args...);
}

奇招二:逗号表达式展开参数包 -- 有点抽象

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的。

printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

template <class T>
int PrintArg(T t)
{
	cout << t << " ";
	return 0;
}

// 展开函数
template <class... Args>
void Cpp_Printf2(Args... args)
{
	// 编译时推导,args...有几个参数,PrintArg就调用几次,就有几个返回值,arr就开多大
	int arr[] = {PrintArg(args)...};
	cout << endl;
}

// 其底层也就是编译器实现的时候,大概是这样的
void Cpp_Printf2(int x, char y, std::string z)
{
	int arr[] = {PrintArg(x), PrintArg(y), PrintArg(z)};
	cout << endl;
}

注:这个可变参数可以加万能引用&&

例如:

template <class... Args>
void Func(Args &&...args); // 注意:万能引用加的位置

8.1、emplace系列的接口和普通的接口的差别

先说结论:推荐使用emplace系列的接口

void test9()
{
	Cpp_Printf(1);
	Cpp_Printf(1, 'A');
	Cpp_Printf(1, 'A', std::string("sort"));



	// 在只传一个参数的时候,emplace系列的接口和普通的接口没什么区别
	kk::string s1("kkkkkk");
	list<kk::string> lt;
	lt.push_back(s1);
	lt.push_back(move(s1));
	kk::string s2("kkkkkk");
	lt.emplace_back(s2);
	lt.emplace_back(move(s2));

	// 真正的区别在于传复合类型的参数时的场景
	list<pair<kk::string, int>> lt2;
	pair<kk::string, int> kv1("lll", 1);
	lt2.push_back(kv1);
	lt2.push_back(move(kv1));

	lt2.emplace_back(kv1);
	lt2.emplace_back(move(kv1));
	// 正常情况下也不会这样去用emplace系列的接口,不然就还是跟普通接口还是没什么区别
	
	// 而是这样写
	lt2.emplace_back("lll", 1); // 这才是emplace系列接口的真正用处
								// 对于普通的接口,在这种场景下,只能先去构造一个pair类型的对象,再进行传参
								// 但是emplace系列的接口支持可变参数,你可以直接给它传构建pair对象所需的参数,然后它自己去创建对象
	// 补充:
	// 上面的push_back(move(kv1))需要进行 一次构造 + 一次移动构造
	// 而这里的emplace_back("lll", 1)只进行了一次构造
	// 但是:移动构造的消耗并不大,所以要记住emplace系列的接口在效率上并不比普通接口高多少。
	// 网上有人说:emplace系列的接口的效率比普通接口更高,这种说法并不完全正确,
	//		 补充:直接 emplace 或者 push/insert (有移动构造的)右值对象 -> 构造 + 移动构造
	//			   直接 emplace 参数包 -> 构造
	//             在有移动构造的深拷贝的对象中,两种接口的效率差别不大。
	//			   因为移动构造的代价很小。
	//			   直接 emplace 或者 push/insert 左值对象/浅拷贝的右值对象 -> 构造 + 拷贝构造
	//			   直接 emplace 参数包 -> 构造
	//			   这种情况下用普通接口的代价就大了很多。
	//			   因为拷贝构造的代价很大。
	//		   注:在没有移动构造的浅拷贝的对象中(只有拷贝构造),两者的效率差别也很大,emplace系列的接口在这种情况下更优。

	// 注:可以去看list的模拟实现,在那里模拟实现了一个emplace的接口

	// 总结:emplace系列的接口的特点是:它们可以做到直接构造,而普通接口只能做到先构造再拷贝构造。
	// 所以:以后在使用容器插入接口(push/insert)的时候,推荐使用emplace系列的,其次emplace能用参数包就用参数包。

	// 补充:单参数也可以用参数包
	list<string> lt3;
	lt3.emplace_back("kkkk");		  // 这里是直接构造
	lt3.emplace_back(string("kkkk")); // 这里是 构造 + 移动构造
}

总结:可变参数模板是个伟大的设计,比如当你在定义一个函数接口时,如果想要这个函数可以接收任意多个参数,就可以选择把该接口写成一个可变参数的函数模板。

9、lambda表达式

在实际生活中,大部分情况下,只进行 int 和 int 之间的比较是没有什么意义的,一般都是进行 自定义类型 和 自定义类型 之间的比较。

但是一般实际中,两个 自定义类型 的比较可能需要考虑很多因素。

比如下面的自定义类型商品,可能需要你按价格比较,也可能需要你按评价去比较。

这时候,在C++11出来之前你只能去写很多个仿函数,给每个比较因素都专门写一个仿函数去实现比较逻辑。

后来觉得这样写太麻烦了,于是就有了lambda表达式。

补充:lambda表达式就像一个匿名函数对象(就是仿函数),本质是一个特殊的,匿名的类类型。它是一个带有operator()的类,即仿函数。

9.1、lambda表达式语法

lambda表达式书写格式:[capture - list](parameters) mutable -> return-type{ statement }

1. lambda表达式各部分说明

[capture - list]:捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

注:捕捉列表不能被省略。

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

注:默认情况下,mutable 可以省略

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

{ statement }:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意:

在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。

因此C++11中最简单的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;
	}
};

void test10()
{
	vector<Goods> v = {{"苹果", 2.1, 5}, {"香蕉", 3, 4}, {"橙子", 2.2, 3}, {"菠萝", 1.5, 4}};
	// 没有 lambda 之前,对 v 的排序方法:
	// sort(v.begin(), v.end(), ComparePriceLess());
	// sort(v.begin(), v.end(), ComparePriceGreater());



	// lambda 使用举例
	// 一个 Add 的 lambda 函数
	[](int a, int b) -> int
	{ return a + b; };
	// 那么该如何使用这个函数呢?(这个函数是匿名的,没有名字)
	auto add1 = [](int a, int b) -> int
	{ return a + b; }; // 方法一:可以用auto
					   //		  因为严格来说lanmda表达式的类型,你不知,我不知,只有编译器知道
	cout << add1(1, 2) << endl;
	cout << typeid(add1).name() << endl; // 想看lambda表达式到底是什么类型,也可以用typeid看,然后会发现看不懂
	auto add2 = [](int a, int b) -> int
	{ return a + b; };
	cout << typeid(add2).name() << endl;
	// 注:即使两个lambda表达式写的一模一样,它们两个的类型也不一样。
	// 原因:lambda表达式的原理类似于范围for,lambda编译时,编译器会生成对应的仿函数,上面的typeid的结果就是这个仿函数的名称,
	//       编译器为了避免生成重复的名称(uuid),所以才会导致,即使两个lambda表达式写的一模一样,你去typeid出来的结果也不一样。
	// 注:通过汇编层面可以发现使用lambda表达式的汇编指令,跟使用仿函数的汇编指令是一样的。

	// 返回值明确的情况下,返回值可以省略,编译器会自动推导
	auto add3 = [](int a, int b)
	{ return a + b; };

	// 没有参数,参数列表也可以省略
	auto func1 = []
	{ cout << "hello world" << endl; };
	func1();



	// 有了lambda表达式之后,上面的商品的sort就可以这样写
	auto priceLess = [](const Goods &gl, const Goods &gr) -> bool
	{ return gl._price < gr._price; }; // 价格的升序
	sort(v.begin(), v.end(), priceLess);
	// 更简洁一点的写法
	sort(v.begin(), v.end(), [](const Goods &gl, const Goods &gr) -> bool // 价格的降序
		 { return gl._price > gr._price; });
	sort(v.begin(), v.end(), [](const Goods &gl, const Goods &gr) -> bool // 评价的升序
		 { return gl._evaluate < gr._evaluate; });
	sort(v.begin(), v.end(), [](const Goods &gl, const Goods &gr) -> bool // 评价的降序
		 { return gl._evaluate > gr._evaluate; });



	// 捕获列表说明 -- 注:被捕捉过来的对象就相当于被当成了一个类的成员变量使用
	int a = 1, b = 2;
	auto swap1 = [](int &x, int &y)
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	swap1(a, b);

	// 捕捉列表可以捕捉当前局部域的对象
	auto swap2 = [a, b]() mutable // 可以不传参数,捕捉a,b对象给lambda用
								  // 注意:这里是传值捕捉,所以这里的a、b的修改并不影响上面的a、b
								  // 传值捕捉过来的对象具有const属性
								  // 要想修改传值捕捉过来的对象,就需要加个mutable去除掉其const属性
								  // 因为这里的a、b是拷贝过来的,即使修改也不改变外面的a、b,所以使用mutable的场景不多
								  // 有没有传值捕捉,都可以加mutable,只是说在没有传值捕捉的时候,mutable就不起作用了。
	{
		int tmp = a;
		a = b;
		b = tmp;
	};
	swap2();

	// 引用方式捕捉(这里有点坑,让人不好区分这里的 & 到底是 取地址 还是 引用)
	auto swap3 = [&a, &b]()
	{
		int tmp = a;
		a = b;
		b = tmp;
	};
	swap3();
	// 注意:不能直接捕捉a、b的地址,但是可以间接捕捉
	int *pa = &a, *pb = &b;
	auto swap4 = [pa, pb]()
	{
		int tmp = *pa;
		*pa = *pb;
		*pb = tmp;
	};
	swap4();
	// 不过,这没有什么意义,有引用方式的捕捉何必多此一举,简直神经

	// 以值传递方式捕捉当前作用域中的所有变量(包括this)
	int q = 1, w = 2, e = 3, r = 4, t = 5;
	auto func2 = [=]()
	{
		return q + w + e + r * t;
	};
	cout << func2() << endl;

	// 以引用传递捕捉当前作用域中的所有变量(包括this)
	auto func3 = [&]()
	{
		++a;
		++b;
		++q;
		++w;
	};

	// 混合捕捉
	auto func4 = [&a, &b, q, w]()
	{
		++a;
		++b;
		// ++q; // 这里 q 没法++,因为是传值捕捉的q,在不加mutable的情况下,传值捕捉过来的对象具有const属性
	};
}

捕捉列表总结:

捕捉列表描述了上下文中哪些数据可以被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函数捕捉列表必须为空。

e.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。

f.lambda表达式之间不能相互赋值,即使看起来类型相同。

补充:函数对象(仿函数)与lambda表达式

  1. 从使用方式上来看,函数对象(仿函数)与lambda表达式完全一样。
  2. 实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象(仿函数)的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

总结:lambda表达式适合用于当前需要一个小函数的场景,需要一个大函数的时候就不适用了。

10、包装器

10.1、function包装器

注:目的就是为了做到类型统一。

function包装器,也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

补充一个概念:可调用对象,包括:

  1. 函数指针(类型定义复杂)
  2. 仿函数对象(要定义一个类,用的时候有点麻烦,且不适合做类型统一)
  3. lambda(从用的角度上,没有类型概念)

那么我们来看看,我们为什么需要function呢?

ret = func(x);

上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?

所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!

为什么呢?继续往下看

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};

void test11()
{
	// 函数名
	cout << useF(f, 11.11) << endl;
	// 函数对象
	cout << useF(Functor(), 11.11) << endl;
	// lamber表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
}

通过上面的程序验证,我们会发现useF函数模板实例化了三份。(count的地址每次打印出来的都不一样)

包装器可以很好的解决上面的问题。

注:std::function在头文件<functional>

类模板原型如下:

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

模板参数说明:

Ret : 被调用函数的返回类型

Args…:被调用函数的形参

注:通过查看function的文档可知,function的底层是一个仿函数,因为它是一个重载了operator()的类。

#include <functional>

// 注:function不是用来定义可调用对象的,是用来包装可调用对象的。

int f1(int a, int b)
{
	return a + b;
}

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

class Plus
{
public:
	static int plusi(int a, int b)
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}
};

void test12()
{
	// 先介绍一下function的用法
	function<int(int, int)> fcn; // 注意:这里的格式跟以前定义一个类模板对象的格式不太一样

	// 还可以这样写
	function<int(int, int)> fcn1(f1);
	function<int(int, int)> fcn2 = f1;
	function<int(int, int)> fcn3 = Functor1();
	function<int(int, int)> fcn4 = [](int x, int y)
	{ return x + y; };

	cout << fcn1(1, 2) << endl;
	cout << fcn1.operator()(1, 2) << endl;
	cout << fcn2(1, 2) << endl;
	cout << fcn3(1, 2) << endl;



	// 包装器就能很好的解决上面的问题(test11)(包装器能够统一类型)
	// 函数名
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;
	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;
	// lambda表达式
	std::function<double(double)> func3 = [](double d) -> double
	{ return d / 4; };
	cout << useF(func3, 11.11) << endl;
	// 此时,count的地址每次打印出来的都是一样的。



	// 包装类的成员函数时,格式:&类名::函数名
	function<int(int, int)> fc1 = &Plus::plusi; // 静态成员函数这样包装就行了(可以不加&,但建议加上,这样好记一点)
												// 静态成员函数是没有this指针的
	cout << fc1(1, 2) << endl;

	function<double(Plus *, double, double)> fc2 = &Plus::plusd; // 非静态成员函数要这样包装(规定必须要加一个&)
																 // 注意:参数要匹配,普通的成员函数前面还有一个this指针
	// 注意:如果function(fc2)是这样写的,则这里不能用匿名对象,匿名对象是右值,没有办法取地址&
	Plus plus;
	cout << fc2(&plus, 1.1, 2.2) << endl; 

	function<double(Plus, double, double)> fc3 = &Plus::plusd;
	// 如果function(fc3)这样写的,这里就可以用匿名对象。
	cout << fc3(Plus(), 1.1, 2.2) << endl; 
	// 注:这里并不是直接把这几个参数传给对应的成员函数,首先,使用成员函数时就不能显示传递参数给 this 。
	// 原因:这里不论是传匿名对象,还是对象指针,其底层都并不是直接将其传给this指针的,而是用这个匿名对象或者函数指针去调用那个成员函数。
}

10.2、bind(绑定)

std::bind函数定义在头文件中,是一个函数模板。

它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。-- 包装一个可调用对象和它的参数

一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。

同时,使用std::bind函数还可以实现参数顺序调整等操作。

原型如下:

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

注:unspecified -- 不确定的,不明确的

功能:用于调整可调用对象的参数个数和参数顺序的。

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

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。

注:arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”。

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

void test_bind1()
{
	auto f1 = sub;
	// cout << typeid(f1).name() << endl; // f1的类型是int(*)(int,int)
	cout << f1(10, 5) << endl;

	// 功能一:调整参数顺序(没啥用,了解即可)
	auto f2 = bind(sub, placeholders::_2, placeholders::_1); // _1永远代表你传的第一个参数,_2永远代表你传的的第二个参数
	cout << f2(10, 5) << endl;								 // 调整顺序的原理:10传给了_1,5传给了_2,然后_1传给了b,_2传给了a
	cout << typeid(f2).name() << endl;						 // 尝试查看一些bind的返回值是什么类型,发现看不懂,不过可以知道的是bind的本质也是一个仿函数,因为它也重载了 operator()
}

class Sub
{
public:
	Sub(int x)
		: _x(x)
	{
	}

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

private:
	int _x;
};

void test_bind2()
{
	// 功能二:调整参数个数(实际就是调整你需要传参的个数)
	auto f3 = bind(&Sub::sub, placeholders::_1, placeholders::_2, placeholders::_3); // 有个参数对应的是 this 指针
	cout << f3(Sub(1), 10, 5) << endl;
	Sub sub1(1);
	cout << f3(&sub1, 10, 5) << endl;

	auto f4 = bind(&Sub::sub, Sub(1), placeholders::_1, placeholders::_2);
	cout << f4(10, 5) << endl;

	auto f5 = bind(&Sub::sub, &sub1, placeholders::_1, placeholders::_2);
	cout << f5(10, 5) << endl;

	auto f6 = bind(&Sub::sub, &sub1, placeholders::_1, 5); // 这样写,就相当于把第三个参数写死了,不能再给第三个参数传东西了
	cout << f6(10) << endl;

	auto f7 = bind(&Sub::sub, &sub1, 10, 5); // 你甚至可以把所有的参数都用bind绑死,这样你传参的时候可以一个都不用传
	cout << f7() << endl;

	// 注:这里和 function 那里是一样的,不论是传匿名对象,还是对象指针,其底层都并不是直接将其传给this指针的,而是用这个匿名对象或者函数指针去调用那个成员函数。
}

void fx(const string &name, int x, int y)
{
	cout << name << " " << "血量:" << x << " " << " 蓝量:" << y << endl;
}

void test_bind3()
{
	// 这里来个贴近实际的栗子
	// 以前使用fx函数都是这样使用的
	fx("卤蛋", 3450, 666);
	fx("卤蛋", 3568, 456);
	fx("卤蛋", 3456, 334);
	fx("卤蛋", 3984, 865);
	// 会发现你一个参数实际上都是固定的,没有变
	// 所以,可以现在可以这样写
	auto f8 = bind(fx, "卤蛋", placeholders::_1, placeholders::_2); // 专门弄一个函数f8给"卤蛋"用
	f8(3450, 666);
	f8(3568, 456);
	f8(3456, 334);
	f8(3984, 865);	
	// 还可以只绑死中间的某一个参数
	auto f9 = bind(fx, placeholders::_1, 5000, placeholders::_2); // 开了锁血挂
	f9("monkey", 600);
	f9("monkey", 624);
	f9("monkey", 646);
	f9("monkey", 656);
}

void test_bind4()
{
	// 补充:
	// 只要是可调用对象都可以用 bind 去绑定
	auto f10 = bind([](int a, int b)
		{ return a - b; }, placeholders::_1, placeholders::_2);
	cout << f10(1, 2) << endl;

	// bind也可以传给function
	function<void(string, int, int)> f11 = bind(fx, placeholders::_1, 5000, placeholders::_2);
	// 注意:传给了function之后,typeid出来的就是function类型了
	cout << typeid(f11).name() << endl;

	// 实际应用中bind还可以用来解决这种问题
	map<string, function<int(int, int)>> opFuncMap = {
		{"+", [](int a, int b)
		 { return a + b; }},
		// {"-",&Sub::sub}, // 这里是编译不过去的,因为sub是普通的成员函数,有个this指针,你这样写会导致参数不匹配
		// 在这种情况中,你也不能去修改上面的fucntion的参数个数,你改了其他的就不匹配了,所以这时候bind就可以派上用场了
		{"-", bind(&Sub::sub, Sub(10), placeholders::_1, placeholders::_2)},
		{"*", [](int a, int b)
		 { return a * b; }},
		{"/", [](int a, int b)
		 { return a / b; }} };
}

// 这里最后介绍一下 bind 原型中的 class Ret的作用(了解即可,很少用)
template <class T>
void fy(int n)
{
	T *p = new T[n];
}

double my_divide(double a, double b)
{
	return a / b;
}

// 请问:这里的fy函数要怎么调用
void test_fy()
{
	// fy(10); // 这样调肯定是调不动的
	fy<int>(10); // 只能显示实例化

	// bind 中的 class Ret 的作用就是让你可以去显示实例化指定 这个绑定的可调用对象的 返回值
	auto func = bind<int>(my_divide, placeholders::_1, placeholders::_2);
	// my_divide 默认的返回值的类型是 double,但经过现在这么一 bind ,类型就变成了 int 。
	cout << func(10, 3) << endl;
}

相关文章:

  • Android设计模式之观察者模式
  • 【IntelliJ IDEA导出WAR包教程】
  • 设计模式-领域模式
  • 享元模式介绍
  • Flutter完整开发实战详解(一、Dart语言和Flutter基础)
  • 当Kafka化身抽水马桶:论组件并发提升与系统可用性的量子纠缠关系
  • Css环形旋转立体感动画
  • 【GNN】第五章:图神经网络架构中的基础设施——GCN、GAT、GraphSAGE、TopKPooling、GAP、GMP
  • AWTK-WEB 快速入门(6) - JS WebSocket 应用程序
  • 通过strApi5的导入导出功能 把sqlite数据转移到mysql
  • 用HTML和CSS生成炫光动画卡片
  • ctf-web:模板注入 -- Cyber Apocalypse CTF 2025 烈火试炼 Trial by Fire
  • Unity学习之Shader(Phong与Blinn-Phong)
  • Java Collection API增强功能系列之五 Map优雅处理键冲突与合并逻辑merge
  • Node.js从0.5到1学习计划
  • 使用Github项目nghttp3的样例学习HTTP/3
  • 新一代ITSM:燕千云重构企业智慧服务生态体系
  • 关于spark在yarn上运行时候内存的介绍
  • 计算机组成原理的学习day01
  • 【面试题】利用Promise实现Websocket阻塞式await wsRequest() 请求
  • 大学生做政府网站/友情链接交换方式有哪些
  • 网站开发代做/图片识别搜索引擎
  • 进一步加强网站建设管理/seo排名赚挂机赚钱软件下载
  • 威海哪里做网站/2023今天的新闻联播
  • 简介常用的网页制作工具/网站优化排名推荐
  • 我想看b站动漫/深圳外贸网站建设