【C++11】锋芒毕露

(续)

一、可变参数模板

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...指出接下来的参数表述零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。

我们来看一个例子:

//参数包表示0-N个参数
template<class ...Args>
void Print(Args&&... args) //args是一个参数包
{
    //这里的sizeof...可以认为是一个新的运算符,专门用来计算参数包中参数的个数,它与sizeof的功能是不一样的
	cout << sizeof...(args) << endl; 
}
int main()
{
	double x = 2.2;
	Print(); //包里有0个参数
	Print(1); //包里有1个参数
	Print(1, string("xxxxx")); //包里有2个参数
	Print(1.1, string("xxxxx"), x); //包里有3个参数

	return 0;
}

可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

若没有可变参数模板,像上面的代码,我们需要写4个函数模板:

void Print()
{}

template<class T1>
void Print(T1&& x1)
{}

template<class T1,class T2>
void Print(T1&& x1,T2&& x2)
{}

template<class T1, class T2,class T3>
void Print(T1&& x1, T2&& x2, T3&& x3)
{}

现在有了可变参数模板,那么上述的任务统统交给编译器,编译器会帮助我们生成上述的4个模板参数然后依次调用,而我们的任务就是写一个可变参数模板即可。

可变参数模板带来的效果:

//根据Print调用情况
//1、首先生成4个函数模板
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)
{}

//2、结合引⽤折叠规则实例化出以下四个函数
void Print()
{}

void Print(int&& arg1)
{}

void Print(int&& arg1, string&& arg2)
{}

void Print(double&& arg1, string&& arg2, double& arg3) //x是左值,所以arg3的类型是左值引用(引用折叠)
{}

可以将可变参数模板理解为模板的模板。 

总结:

模板:一个函数模板可以实例化出多个不同类型参数的函数(类型可变)

可变参数模板:一个可变参数模板函数可以实例化出多个不同参数个数的函数模板(类型可变+个数可变)

二、包扩展

通过上面的学习,我们可以将包中的参数个数打印出来,那能不能将包中的内容打印出来呢(就是把参数取出来)?

思路:

template<class ...Args>
void Print(Args&&... args)
{
	for (int i = 0;i < sizeof...(args);++i)
		cout << args[i] << " "; 
	cout << endl;
}

这样写法看上去可以,但是,它是C++是不支持这样写的,有一点就直接否定了:包中每个参数的类型不同。类型都不同怎么可能像数组这样使用(args[i]),数组也必须保证里面的元素类型相同。所以这种方法是不可取的。

解决方法:包扩展(解析出参数包的内容)

方式一、

//包扩展(解析出参数包的内容)
//方式一、
void ShowList()  //参数包中参数个数为0,直接匹配这个函数
{
	cout << endl; 
}

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

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.1, string("xxxxx"), x); //包里有3个参数

	return 0;
}

运行结果:

我简单来说一下这段代码的执行过程:

首先,执行Print()时,参数包中参数个数为0,那么就会直接调用void ShowList()这个函数,打印'\n';执行Print(1),参数包中参数个数为1,会调用void ShowList(T&& x, Args&&... args)这个函数模板,首先打印第一个参数内容,然后调用ShowList(args...),此时args这个参数包中参数个数就像对于之前减少了1,那就是0,于是调用void ShowList(),打印'\n',结束;执行Print(1, string("xxxxx")),参数包中参数个数为2,会调用void ShowList(T&& x, Args&&... args)这个函数模板,首先打印第一个参数内容,然后调用ShowList(args...),此时args这个参数包中参数个数就像对于之前减少了1,那就是1,于是再调用void ShowList(T&& x, Args&&... args),打印第一个参数内容(原参数包第二个参数的内容),然后调用ShowList(args...),此时args这个参数包中参数个数就像对于之前减少了1,那就是0,于是调用void ShowList(),打印'\n',结束;执行Print(1.1, string("xxxxx"), x)的过程和上面类似。

上述描述就是包展开的过程,包展开的过程是在程序编译阶段完成的,并非运行时。

为什么说是编译阶段完成的呢?

因为ShowList(T&& x, Args&&... args)这是一个函数模板,函数模板在确定类型时是在编译阶段完成的,所以,包展开的过程是在程序编译阶段完成的。

我们结合示意图来理解一下:

图中右边出现的函数统统在编译阶段由编译器实例化出来的!!!然后程序运行直接调用实例化出来的函数。  

有人会觉得上述逻辑有点麻烦,可以这样写:

template <class T, class ...Args>
void ShowList(T&& x, Args&&... args)
{
	if (sizeof...(args) == 0)
		return;

	cout << x << " ";
	ShowList(args...);
}

但是,这样写是错误的,包展开的过程是在编译阶段完成的,而if判断这句代码是在程序运行时才执行的,所以这样写的逻辑是不对的。

我们也可以不用模板,直接主动写出具体函数:

void ShowList()
{
	cout << endl; 
}

void ShowList(double x)
{
	cout << x << " ";
	ShowList();
}

void ShowList(string x, double z)
{
	cout << x << " ";
	ShowList(z);
}

void ShowList(int x, string y, double z)
{
	cout << x << " ";
	ShowList(y, z);
}

void Print(int x, string y, double z)
{
	ShowList(x, y, z);
}

int main()
{
	Print(1, string("xxxxx"), 2.2);
	return 0;
}

运行结果: 

上述写法就是我们自己写,程序运行直接调用即可;如果我们写的是模板,那么执行"Print(1, string("xxxxx"), 2.2)"时,编译器就会在编译阶段将可变参数模板通过模式的包扩展,推导出上面三个重载函数(ShowList),也就是编译器在底层帮我们实现并调用。

所以,我们在写代码时只用写一个模板即可,剩下的工作交给编译器,编译器所做的工作量是巨大的,因为它需要通过模板来实例化出实际有意义的函数,模板的作用就是减少了我们的工作量,增加的编译器的工作量,(编译器"累点"没关系,我们轻松就行^____^)。模板是不会改变效率问题的,也可以将模板理解为:"模板是写给编译器的"。

方式二、

//包扩展(解析出参数包的内容)
//方式二、
template <class T>
int GetArg(const T& x)
{
	cout << x << " ";
	return 0;  //任意返回
}

template <class ...Args>
void Arguments(Args... args) //充当"跳板"
{}

template <class ...Args>
void Print(Args... args)
{
	//注意GetArg必须有返回值,这样才能组成参数包给Arguments
	Arguments(GetArg(args)...); //注意语法格式
}

int main()
{
	Print(1, string("xxxxx"), 2.2);
	return 0;
}

运行结果:

这种方式不会有发生递归, 利用Arguments当"跳板",执行3次GetArg,即可解析出args包里的内容。Arguments必须存在,Arguments存在,编译器就需要对其参数个数进行推导,一旦推导,那GetArg就会将包中的数据统统解析(打印)出来。

编译器会将Print处理为如下结果:

void Print(int x, string y, double z)
{
     Arguments(GetArg(x), GetArg(y), GetArg(z));
}

其实Print也可以这样写:

template <class ...Args>
void Print(Args... args)
{
	int arr[] = { GetArg(args)... };
}

如果这样写,要保证GetArg的返回类型是整形;因为要推导出arr数组到底有多大,就必须将包里的数据遍历完。

三、emplace系列接口

C++11以后STL容器新增了emplace系列的接口,emplace系列的接口均为可变参数模板,如emplace_back,它的功能与push_back类似,也是插入数据,但是它们之间有不同的地方。

我们以list容器为例(每个容器基本都有emplace_back接口):

我们来看看这两个接口到底有什么不同的地方:

外部条件:emplace_back调用时需要传一个参数包;push_back调用时需要传一个对象。

内部调用:

我们先自己写一个string,方便后续观察现象:

namespace blue
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin() {
			return _str;
		}
		iterator end() {
			return _str + _size;
		}
		const_iterator begin() const {
			return _str;
		}
		const_iterator end() const {
			return _str + _size;
		}
		//构造
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str)-构造" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;
			reserve(s._capacity);
			for (auto ch : s)
				push_back(ch);
		}

		void swap(string& ss)
		{
			std::swap(_str, ss._str);
			std::swap(_size, ss._size);
			std::swap(_capacity, ss._capacity);
		}

		//移动构造
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			// 转移掠夺你的资源
			swap(s);
		}

		//赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 赋值重载" <<
				endl;
			if (this != &s)
			{
				_str[0] = '\0';
				_size = 0;
				reserve(s._capacity);
				for (auto ch : s)
					push_back(ch);
			}
			return *this;
		}

		//移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}

		~string()
		{
			//cout << "~string() -- 析构" << endl;
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				if (_str)
				{
					strcpy(tmp, _str);
					delete[] _str;
				}
				_str = tmp;
				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity *
					2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const {
			return _str;
		}
		size_t size() const {
			return _size;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

通过接口调用来观察它们两者的区别:

调用形式一:

int main()
{
	list<blue::string> lt;
	blue::string s1("111111111111");
	cout << "--------------------------" << endl;

	//传左值,emplace_back和push_back一样,走拷贝构造,两者没有区别
	lt.emplace_back(s1);
	cout << "--------------------------" << endl;
	lt.push_back(s1);
	cout << "--------------------------" << endl;
    
    return 0;
}

运行结果:

传左值,emplace_back和push_back一样,走拷贝构造,两者在效率方面没有任何区别。

调用形式二:

int main()
{
	list<blue::string> lt;
	blue::string s1("111111111111");
	cout << "--------------------------" << endl;

	//传右值,emplace_back和push_back一样,走移动构造,两者没有区别
	lt.emplace_back(move(s1));
	cout << "--------------------------" << endl;
	lt.push_back(move(s1));
	cout << "--------------------------" << endl;

    return 0;
}

运行结果:

传右值,emplace_back和push_back一样,走移动构造,两者没有区别。

调用形式三:

int main()
{
	list<blue::string> lt;
	blue::string s1("111111111111");
	cout << "--------------------------" << endl;

	lt.emplace_back("111111111111");  //不走隐式类型转换
	cout << "--------------------------" << endl;
	lt.push_back("111111111111");	//直接传参,走隐式类型转换
	cout << "--------------------------" << endl;

	return 0;
}

运行结果:

这时,它们两个的调用结果就不一样了。

对于push_back来说:

在实例化lt时就确定了value_type是blue::string类型,所以调用push_back时,value_type此时就是blue::string类型,而我们的参数是const char*,参数不匹配,所以首先要走隐私类型转换,那么就要调blue::string的构造函数 ,生成临时对象,临时对象是右值,所以会调用void push_back (value_type&& val),此时的val就是右值引用,然后一层一层往下传,最终会调用blue::string的移动构造。

对于emplace_back来说:

在实例化lt时,Args是什么类型是无法确定的,只有传参时才可推导出Args的具体类型,所以调用emplace_back时,不会隐式类型转换,Args此时的类型是const char*,然后层层向下传,最终调用blue::string的构造。

所以,对于这种情况,emplace_back和push_back是有差别的,push_back比emplace_back多了一个移动构造,效率方面其实影响也不大,因为移动构造的代价极低。但是,但是如果对于浅拷贝的类型呢?比如list中的元素类型是Date,Date类中没有移动构造和移动赋值(也不需要),那么push_back就会比emplace_back多了一个拷贝构造,那么emplace_back会更快一点点。注意:这里所说的快一点点,其实可以忽略不计,效率几乎不会受到影响。

调用形式四:

int main()
{
	list<pair<blue::string, int>> lt1;
	pair<blue::string, int> kv("苹果", 1);
	cout << "--------------------------" << endl;

	//传左值,emplace_back和push_back一样,走拷贝构造,两者没有区别
	lt1.emplace_back(kv);
	cout << "--------------------------" << endl;
	lt1.push_back(kv);
	cout << "--------------------------" << endl;
    
    return 0;
}

运行结果:

传左值,emplace_back和push_back一样,走拷贝构造,两者在效率方面没有任何区别。 

调用形式五: 

int main()
{
	list<pair<blue::string, int>> lt1;
	pair<blue::string, int> kv("苹果", 1);
	cout << "--------------------------" << endl;

	//传右值,emplace_back和push_back一样,走移动构造,两者没有区别
	lt1.emplace_back(move(kv));
	cout << "--------------------------" << endl;
	lt1.push_back(move(kv));
	cout << "--------------------------" << endl;

    return 0;
}

运行结果: 

传右值,emplace_back和push_back一样,走移动构造,两者没有区别。 

调用形式六:

int main()
{
	list<pair<blue::string, int>> lt1;
	pair<blue::string, int> kv("苹果", 1);
	cout << "--------------------------" << endl;


	//lt1.emplace_back({ "苹果", 1 }); //不支持,编译器无法推导出Args具体类型
	lt1.emplace_back("苹果", 1 ); //参数包,层层往下传,最终调用构造生成结点(因为pari支持2参构造)
	cout << "--------------------------" << endl;
	//lt1.push_back("苹果", 1); //不支持
	lt1.push_back({ "苹果", 1 }); //隐式类型转换,先构造pair的临时对象,最后层层下传最终调用移动构造生成结点
	cout << "--------------------------" << endl;

    return 0;
}

运行结果:

所以相较于插入来说,还是emplace_back要比push_back略快一些。 

总结:

emplace系列兼容push系列和insert的功能,部分场景下emplace可以直接构造,push和insert是构造+移动构造或构造+拷贝构造,所以emplace综合而言更好用、更强大。

故推荐emplace系列接口替代push和insert系列接口。

四、lambda表达式

🐱‍🏍基本介绍

lambda表达式本质是⼀个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。

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

lambda表达式的语法格式:

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

[capture-list]:捕捉列表,该列表总是出现在lambda表达式的开始位置,编译器根据[]来判断接下来的代码是否为lambda表达式,捕捉列表能够捕捉上下文中的变量供lambda函数使用,捕捉列表可以传值和传引用捕捉,捕捉列表可以为空,但捕捉列表在任何情况下都不可省略。

(parameters):参数列表,与普通函数的参数列表功能类似;如果不需要参数传递,则可以连同()⼀起省略。

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

{function boby}:函数体,函数体内的实现跟普通函数完全类似;在该函数体内,除了可以使用参数列表的参数外,还可以使用所有捕获到的变量,函数体可以为空,但函数体在任何情况下都不可省略。  

我们先来写一个简单的lambda表达式来帮助理解:

int main()
{
    //用auto自动推导add1类型即可,我们无法知道add1的具体类型名,也不需要知道
    //        捕获列表 参数列表  返回类型      函数体
	auto add1 = [](int x, int y)->int {  return x + y; };  //add1是一个lambda对象
	cout << add1(1, 2) << endl;

    return 0;
}

运行结果:

是不是也没有那么复杂?我们再来写一个:

int main()
{
	//1、捕捉列表为空也不能省略
	//2、参数列表为空可以省略
	//3、返回值可以省略,可以通过返回对象自动推导
	//4、函数体不能省略
	auto func1 = []
	{
		cout << "hello bit" << endl;
		return 0;
	};

	func1();

	return 0;
}

运行结果:

通过上面两个例子,相信大家已经了解了lambda的基本使用方法,接下来,我们来看一看捕捉列表到底有什么作用。

🐱‍🏍捕捉列表

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

🐱‍👤捕捉方式一:

第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分隔;比如:[x,y,&z],其中x和y是值捕捉,z是引用捕捉。通常情况下,引用都是和类型绑在一起的,&z大家第一眼可能认为是取地址,但在捕捉列表中&表示引用,这点是特殊的,大家不要记错。

使用值捕捉的变量不能被修改(默认被const修饰)。

如果将a、b定义到lambda表达式下面,会报错,因为编译器走到捕捉列表时,只会向上查找,不会向下查找。

特殊地,在类域中,若要使用某个变量会在整个类域中查找。

在lambda的函数体中可以直接使用全局域的东西,不需要通过捕捉列表捕捉,也不能捕捉:

注意:同一个变量不能捕捉两次。 

🐱‍👤捕捉方式二: 

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

int main()
{    
    int a = 0, b = 1, c = 2, d = 3;
	//隐式值捕捉 - 用了哪些变量就捕捉那些变量
	auto func2 = [=]
	{
		//a++; //err,值捕捉就不能修改捕捉到的变量

		int ret = a + b + c;
		return ret;
	};
	cout << func2() << endl;

	//隐式引用捕捉 - 用了哪些变量就捕捉哪些变量
	auto func3 = [&]
	{
		//引用捕捉可以修改捕捉到的变量
		a++;
		c++;
		d++;

		//e++; //err,必须确保使用的变量能被捕捉到,否则就报错
	};
	func3();

    return 0;
}

🐱‍👤捕捉方式三:

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

int main()
{
    int a = 0, b = 1, c = 2, d = 3;
	//混合捕捉1
	auto func4 = [&, a, b]
	{
		//a、b是值捕捉,故不能修改
		//a++;
		//b++;
		c++;
		d++;
		return a + b + c + d;
	};
	cout << func4() << endl;

	//混合捕捉2
	auto func5 = [=, &a, &b]
	{
		
		a++;
		b++;
		//c、d是值捕捉,故不能修改
		//c++;
		//d++;
		return a + b + c + d;
	};
	cout << func5() << endl;

	return 0;
}

注意: 

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

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

🐱‍🏍lambda的应用

在学习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 Compare1
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};
struct Compare2
{
	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(), Compare1());
	sort(v.begin(), v.end(), Compare2());


	//那么这里lambda就很好用了(如果是仿函数,就需要写上4个),这里并不需要担心命名风格,因为lambda表达式可以直观的看出来具体功能
	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在其它很多地方也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等, lambda的应用还是很广泛的,以后我们会不断接触到。

🐱‍🏍lambda的原理

lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有lambda和范围for这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了一个lambda以后,编译器会生成⼀个对应的仿函数的类。

仿函数的类名是编译按一定规则生成的,保证不同的lambda生成的类名不同;lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda这个仿函数的构造函数的实参。我们来看个例子:

当然,口说无凭,我们可以根据底层汇编代码来看看到底它们两个本质上是不是 一样的,底层汇编代码可不会骗人哦。

所以lambda的底层原理就是一个仿函数。 就上面而言,r2其实就是一个仿函数对象,r2(10000,2)就是调用仿函数中的operator()。

五、类的新功能

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

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

特别地,如果没有自己实现移动构造函数,没有实现析构函数、拷贝构造、拷贝赋值重载,那么这时编译器会自动生成⼀个默认移动构造(这点与先前系统默认生成成员函数的条件有所不同)。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用它的移动构造,没有实现就调用它的拷贝构造。

特别地,如果没有自己实现移动赋值重载函数,没有实现析构函数、拷贝构造、拷贝赋值重载,那么这时编译器会自动生成⼀个默认移动赋值(这点与先前系统默认生成成员函数的条件有所不同)。默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用它的移动赋值,若没有实现就调用它的赋值重载。(默认移动赋值跟上面移动构造完全类似)

如果提供了移动构造或者移动赋值,编译器就不会自动生成拷移动构造和移动赋值了。

举个例子:

class Person
{
public:
	Person(const char* name = "张三", int age = 1)
		:_name(name)
		, _age(age)
	{}
private:
	blue::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = std::move(s1);

	Person s3;
	s3 = std::move(s2);
	return 0;
}

此时,Person类中没有写移动构造和移动赋值,因为此时我们没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,所以系统会默认生成移动构造和移动赋值。

我们来看运行结果:

结果正如我们上面所说的那样, 默认生成的移动构造/移动赋值,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则会调用它的移动构造/移动赋值。

若果我们写上一个析构函数:

class Person
{
public:
	Person(const char* name = "张三", int age = 1)
		:_name(name)
		, _age(age)
	{}
    
    //添加析构函数
	~Person()
	{}

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

int main()
{
	Person s1;
	Person s2 = std::move(s1);

	Person s3;
	s3 = std::move(s2);
	return 0;
}

运行结果:

这时,系统就不会默认生成移动构造和移动赋值了。但会自动生成拷贝构造和赋值重载,默认生成的拷贝构造/赋值重载,对于内置类型成员会执行逐成员按字节拷贝,对于自定义类型成员,则会调用它的拷贝构造/赋值重载。

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

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

例如:

👶defult和delete

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

class Person
{
public:
	Person(const char* name = "张三", int age = 1)
		:_name(name)
		, _age(age)
	{}

	~Person()
	{}

    //有析构函数了,系统就不会默认生成移动构造了
    Person(Person&& p) = default;  //强制生成移动构造

    //如果有移动构造,但没写拷贝构造,那么系统也不会默认生成拷贝构造了(移动构造会影响拷贝构造)
	Person(const Person& p) = default; //强制生成拷贝构造
	
private:
	blue::string _name;
	int _age;
};

在C++中,我们有时候不希望一个类可以被拷贝如(istream、ostream等),如果能想要限制某些默认函数的生成调用,那么在C++98中,是将该函数设置成private并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明后加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。比如ostream就不允许拷贝:

func修改如下可以正常调用:

void func(ostream& out)  
{}

👶 final与override

这两个关键字也是C++11新增的,我在这篇文章中有介绍 -> 【C++】多态

👶STL中一些变化

C++11中,STL中一些新的变化分为两个方面:

  • 新容器
  • 新接口

下图1圈起来的就是C++11中STL增加的新容器,但是实际最有用的容器就是unordered_map和unordered_set。这两个容器我在前面文章已经进行了非常详细的讲解,大家有兴趣可以去看看,其它的容器大家了解⼀下即可。

STL中容器也增加不少新接口,容器的push/insert/emplace系列接口都增加了与移动构造和移动赋值相关的接口,如果传的是右值,那么push/insert/emplace系列接口的效率就会提高,还有比如initializer_list版本的构造,以及范围for等。

initializer_list和范围for,它们并不会改变效率,只是书写形式上变得更简单了,它们的存在对于C++98来说是锦上添花

其中STL最核心的变化有两点:

  1. unordered_map和unordered_set的出现
  2. emplace系列接口和push、insert右值引用版本的接口

这两点核心变化对于C++98来说就是雪中送炭

六、包装器

🎈function

std::function是一个类模板,也是一个包装器。 用std::function实例化出来的对象可以包装存储其它的可调用对象,包括函数指针、仿函数、 lambda 、 bind表达式等,存储的可调用对象被称为std::function的目标,若std::function不含目标,则称它为空。function被定义在<functional>这个头文件中。

我们先来看一下function的用法:

#include<functional>

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

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

int main()
{
	//包装各种可调用对象
	function<int(int, int)> f1 = f; //int是返回值类型 (int ,int)是形参类型 包装函数指针
	function<int(int, int)> f2 = Functor(); //包装仿函数对象
	function<int(int, int)> f3 = [](int a, int b)->int { return a + b; }; //包装lambda对象
	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;

	return 0;
}

运行结果:

通过包装器就可以将不同类型可调用对象统一起来。 

除此之外,它还可以包装成员函数:

#include<functional>
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()
{
	//1、
	function<int(int, int)> f4 = &Plus::plusi; //包装静态成员函数,静态成员函数要指明类域(&可加可不加)
	cout << f4(1, 1) << endl;

	//2、
	function<double(Plus*, double, double)> f5 = &Plus::plusd; //包装成员函数,成员函数要指明类域并且前面加&才能获取地址
	Plus pl;
	cout << f5(&pl, 1.111, 1.1) << endl;

	//3、
	function<double(Plus, double, double)> f6 = &Plus::plusd;
	cout << f6(pl, 1.1, 1.1) << endl;
	cout << f6(Plus(), 1.1, 1.1) << endl;

	//4、
	function<double(Plus&&, double, double)> f7 = &Plus::plusd;
	cout << f7(move(pl), 1.1, 1.1) << endl;
	cout << f7(Plus(), 1.1, 1.1) << endl;

	return 0;
}

🎈bind

bind是一个函数模板,它也是⼀个可调用对象的包装器,可以把它看做是⼀个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。 bind可以用来调整参数个数和参数顺序,它也在<functional>这个头文件中。

调用bind的一般形式:auto newCallable = bind(callable,arg_list); 其中newCallable本身是一个可调用对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数(callable是一个调用对象)。当我们调newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

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

我们来看一下它的用法:

1、调整参数顺序

#include <functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

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

int main()
{
	auto sub1 = bind(Sub, _1, _2); //Sub是一个可调用对象(函数指针)
	cout << sub1(10, 5) << endl;  //可以理解为:10对应_1即Sub第一个参数,5对应_2即Sub第二个参数

	//调整参数位置,_1始终代表第一个实参,_2始终代表第二个实参
	auto sub2 = bind(Sub, _2, _1);
	cout << sub2(10, 5) << endl; //可以理解为:10对应_1即Sub第二个参数,5对应_2即Sub第一个参数

	return 0;
}

运行结果:

在实际应用中,调整参数的用途不大。 

2、调整参数个数(常用)

#include <functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

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

int main()
{
	//调整参数个数(常用)
	auto sub3 = bind(Sub, 100, _1);  //将参数a"绑死",a是固定不变的,始终是100
	cout << sub3(5) << endl;

	auto sub4 = bind(Sub, _1, 100);  //将参数b"绑死",b是固定不变的,始终是100
	cout << sub4(5) << endl;
	return 0;
}

也可以这么玩:

int main()
{
	function<double(Plus&&, double, double)> f1 = &Plus::plusd;
	cout << f1(Plus(), 1.1, 1.1) << endl;

	//将成员函数对象进行绑死,就不需要每次都传递了
	function<double(double, double)> f2 = bind(&Plus::plusd, Plus(), _1, _2);
	cout << f2(1.1, 1.1) << endl;

    return 0;
}

bind返回的是一个可调用对象,function可以包装任何可调用对象,所以可以联合起来使用。 

七、结语

本篇内容到这里就结束啦,希望对大家有帮助,C++11还有一个重要的东西,那就是智能指针,关于智能指针,我会专门写一篇文章讲述它,这里就不多说了🙊,最后祝各位生活愉快🙌!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/922876.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

用户管理(MySQL)

目录 1用户管理&#xff08;MySQL&#xff09; 1.1 用户 1.1.1 用户信息 1.1.2 创建用户(后%是可以任意远端登录) 1.1.3 刷新一下 1.1.4 删除用户 1.1.5 修改用户密码 1.2 数据库的权限 1.2.1 登录创建用户 1.2.2给权限 1.2.2.1 把jj数据库中uu表的权限给woaini这个…

Hive离线数仓结构分析

Hive离线数仓结构 首先&#xff0c;在数据源部分&#xff0c;包括源业务库、用户日志、爬虫数据和系统日志&#xff0c;这些都是数据的源头。这些数据通过Sqoop、DataX或 Flume 工具进行提取和导入操作。这些工具负责将不同来源的数据传输到基于 Hive 的离线数据仓库中。 在离线…

Linux——Uboot命令使用

什么是Uboot&#xff1f; 1&#xff09;Uboot是一个裸机程序&#xff0c;比较复杂。类似我们PC机的BIOS程序。 2&#xff09;Uboot就是一个bootloader&#xff0c;作用就是用于启动Linux或者其他系统&#xff0c;Uboot最主要的工作是初始化DDR&#xff0c;因为Linux的运行是运行…

Cannal实现MySQL主从同步环境搭建

大家好&#xff0c;我是袁庭新。 在多数情况下&#xff0c;客户端往往会优先获取缓存中的数据。然而&#xff0c;当缓存数据与数据库中的实际数据存在显著不一致时&#xff0c;可能会导致严重的后果。因此&#xff0c;确保数据库与缓存数据之间的一致性变得至关重要&#xff0c…

C++《二叉搜索树》

在初阶数据结构中我学习了树基础的概念以及了解了顺序结构的二叉树——堆和链式结构二叉树该如何实现&#xff0c;那么接下来我们将进一步的学习二叉树&#xff0c;在此会先后学习到二叉搜索树、AVL树、红黑树&#xff1b;通过这些的学习将让我们更易于理解后面set、map、哈希等…

C++ —— 以真我之名 如飞花般绚丽 - 智能指针

目录 1. RAII和智能指针的设计思路 2. C标准库智能指针的使用 2.1 auto_ptr 2.2 unique_ptr 2.3 简单模拟实现auto_ptr和unique_ptr的核心功能 2.4 shared_ptr 2.4.1 make_shared 2.5 weak_ptr 2.6 shared_ptr的缺陷&#xff1a;循环引用问题 3. shared_ptr 和 unique_…

springboot项目使用maven打包,第三方jar问题

springboot项目使用maven package打包为可执行jar后&#xff0c;第三方jar会被打包进去吗&#xff1f; 答案是肯定的。做了实验如下&#xff1a; 第三方jar的项目结构及jar包结构如下&#xff1a;&#xff08;该第三方jar采用的是maven工程&#xff0c;打包为普通jar&#xf…

第六届智能控制、测量与信号处理国际学术会议 (ICMSP 2024)

重要信息 2024年11月29日-12月1日 中国陕西西安石油大学雁塔校区 大会官网&#xff1a;www.icmsp.net 大会简介 第六届智能控制、测量与信号处理国际学术会议&#xff08;ICMSP 2024&#xff09;由西安石油大学、中海油田服务股份有限公司、浙江水利水电学院与中国石油装备…

设计LRU缓存

LRU缓存 LRU缓存的实现思路LRU缓存的操作C11 STL实现LRU缓存自行设计双向链表 哈希表 LRU&#xff08;Least Recently Used&#xff0c;最近最少使用&#xff09;缓存是一种常见的缓存淘汰算法&#xff0c;其基本思想是&#xff1a;当缓存空间已满时&#xff0c;移除最近最少使…

跨平台应用开发框架(1)----Qt(组件篇)

目录 1.Qt 1.Qt 的主要特点 2.Qt的使用场景 3.Qt的版本 2.QtSDK 1.Qt SDK 的组成部分 2.安装 Qt SDK 3.Qt SDK 的优势 3.Qt初识 1.快速上手 widget.cpp mian.cpp widget.h Helloworld.pro 2.对象树 3.坐标系 4.信号和槽 1. 信号和槽的基本概念 2. 信号和槽的…

Vue3+SpringBoot3+Sa-Token+Redis+mysql8通用权限系统

sa-token支持分布式token 前后端代码&#xff0c;地球号: bright12389

专题二十三_动态规划_回文串系列问题_算法专题详细总结

目录 动态规划 回文串系列问题 1. 回⽂⼦串&#xff08;medium&#xff09; 解析&#xff1a; 解决回文串问题&#xff0c;这里提供三个思路&#xff1a; 1.中心扩展法&#xff1a;n^2 / 1 2.马拉车算法&#xff1a;n / n 3.动态规划算法&#xff1a;n^2 / n^2 1.状态表…

ES实用面试题

一、es是什么&#xff0c;为什么要用它&#xff1f; ES通常是Elasticsearch的简称&#xff0c;它是一个基于Lucene构建的开源搜索引擎。Elasticsearch以其分布式、高扩展性和实时数据分析能力而闻名&#xff0c;广泛用于全文搜索、日志分析、实时监控等多种场景。 基本特点&am…

实现在两台宿主机下的docker container 中实现多机器通讯

基于我的实验背景 上位机&#xff1a;ubuntu 20.04 (docker humble 22.04) 下位机&#xff1a;ubuntu 22.04&#xff08;docker noetic 20.04&#xff09; 目标&#xff1a;实现在上位机中的docker container 容器的22.04环境去成功远程访问 非同网段的下位机的20.04的contai…

FakeLocation Linux | Windows关于使用教程一些规范说明

前言:使用教程&#xff08;FakeLocation版本请使用1.2.xxx&#xff09;| (1.3.xxx 未测试) 环境模块&#xff0c;是指代FakeLocation开启以后会把环境弄的异常,环境模块可以保证环境安全Dia 作为软件需要在Lsp框架里面勾选激活使用&#xff0c;并且开启增强模式FakeLocation 请…

指针的奥秘:深入探索内存的秘密

前言 在计算机编程的广阔天地中&#xff0c;指针作为一种独特的数据类型&#xff0c;它不仅是C语言的核心&#xff0c;也是理解计算机内存管理的基石。指针的概念虽然强大&#xff0c;但对于初学者来说&#xff0c;它常常是学习过程中的一个难点。本文旨在揭开指针的神秘面纱&a…

Mairadb 最大连接数、当前连接数 查询

目录 查询数据库 最大连接数 查询当前连接总数 环境 Mariadb 10.11.6 跳转mysql数据库&#xff1a; 查询数据库 最大连接数 show variables like max_connections; 注意; 这个版本不能使用 &#xff1a; show variables like ‘%max_connections%’; 会报错 &#xff…

电影风格城市夜景旅拍Lr调色教程,手机滤镜PS+Lightroom预设下载!

调色教程 电影风格城市夜景旅拍通过 Lightroom 调色&#xff0c;将城市夜晚的景色打造出如同电影画面般的质感和氛围。以独特的色彩和光影处理&#xff0c;展现出城市夜景的魅力与神秘。 预设信息 调色风格&#xff1a;电影风格预设适合类型&#xff1a;人像&#xff0c;街拍…

代码管理之Gitlab

文章目录 Git基础概述场景本地修改未提交&#xff0c;拉取远程代码修改提交本地&#xff0c;远程已有新提交 GitIDEA引入Git拉取仓库代码最后位置 Git基础 概述 workspace 工作区&#xff1a;本地电脑上看到的目录&#xff1b; repository 本地仓库&#xff1a;就是工作区中隐…

【FPGA】Verilog:利用 4 个串行输入- 串行输出的 D 触发器实现 Shift_register

0x00 什么是寄存器 寄存器(Register)是顺序逻辑电路中使用的基本组成部分之一。寄存器用于在数字系统中存储和处理数据。寄存器通常由位(bit)构成,每个位可以存储一个0或1的值。通过寄存器,可以设计出计数器、加法器等各种数据处理电路。 0x01 寄存器的种类 基于 D 触发…