您现在的位置是:首页 >其他 >【C++】c++11的新特性——右值引用/移动语义/lambda表达式网站首页其他

【C++】c++11的新特性——右值引用/移动语义/lambda表达式

超人不会飞) 2024-10-25 00:01:07
简介【C++】c++11的新特性——右值引用/移动语义/lambda表达式


  • ? 个人主页超人不会飞)
  • ? 本文收录专栏:《C++的修行之路》
  • ? 如果本文对您有帮助,不妨点赞、收藏、关注支持博主,我们一起进步,共同成长!

C++11介绍

C++11是C++的第二个主要版本,也是自C++98以来最重要的更新。引入了大量的变化,旨在规范现有的实践,并改善C++程序员可用的抽象。在ISO于2011年8月12日最终批准之前,它的名称为“C++0x”,因为人们预计它将在2010年之前发布。C++03到C++11之间历时8年,因此这成为迄今为止版本之间最长的间隔。自那时以来,C++定期每3年进行更新。

?c++11的官方文档

1. 统一的列表初始化

1.1 {}初始化

  • C++98中,可以用花括号{}对数组或结构体元素进行列表初始值设定。

    struct Point
    {
    	int x;
    	int y;
    };
    
    int main()
    {
    	int a[] = { 1,2,3,4,5 }; //构造数组
    	Point p = { 0,1 }; //依次构造Point中的元素
    	return 0;
    }
    
  • C++11扩大了{}列表初始化的范围。使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

    struct Point
    {
    	int x;
    	int y;
    };
    int main()
    {
    	int x1 = 1;
    	int x2{ 2 };
    
    	int array1[]{ 1, 2, 3, 4, 5 };
    	int array2[5]{ 0 };
    
    	Point p{ 1, 2 };
    
    	// C++11中列表初始化也可以适用于new表达式中
    	int* pa = new int[4] { 1, 2, 3, 4};
    
    	return 0;
    }
    
  • 对于自定义类型,实例化对象时,列表应用于构造函数的初始化列表

    class Date
    {
    public:
    	Date(int year, int month, int day)
    		:_year(year)
    		, _month(month)
    		, _day(day)
    	{}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	//传统的构造方式
    	Date d0(2000, 1, 1);
    
    	//列表初始化构造
    	Date d1 = { 2023,5,5 };//传入Date的构造函数
    	Date d2{ 1999,9,9 };
    	return 0;
    }
    

1.2 std::initializer_list

?C++11中,对于STL容器,支持以下的初始化方法

vector<int> v = { 2,3,4,5,2,1 };
list<int> l = {2,3,4,5,2,1};
map<string, string> m = { {"苹果","apple"}, {"香蕉","banana"}};

?这是因为C++11引入了initializer_list这一类型,并为STL容器类型提供了以initializer_list类型对象为参数的构造函数。实例化STL容器对象时,将{}括起来的列表视为一个initializer_list类型的对象,调用对应的构造函数即可。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

std::initializer_list是一个模板类型,initializer_list<T>是一个以const T类型元素组成的序列类型。因此,可以将其视作一个常量数组,不可以修改。

std::initializer_list使用场景:

  • std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。
  • 也可以作为operator=的参数,这样就可以用大括号赋值。

⭕使用演示

void test1()
{
	initializer_list<int> il = { 2,4,1,5,6,3 };//可以理解为一个常量数组

	auto it = il.begin();
	//*it = 2;// const类型,不可修改

	vector<int> v = { 2,3,4,5,2,1 };
    
	//等价于
	//initializer_list<int> il = { 2,3,4,5,2,1 };
	//vector<int> v(il);
    
    //还可以构造匿名对象
    vector<int>{1,2,3};
    //等价于vector<int> v{1,2,3}/vector<int> v = {1,2,3};

	map<string, string> m = { {"苹果","apple"}, {"香蕉","banana"},{"西瓜","watermelon"} };
	//等价于
	//initializer_list<pair<const string,string>> il_str = { {"苹果","apple"}, {"香蕉","banana"},{"西瓜","watermelon"} };
	//map<string, string> m(il_str);

	// map<string, string> m = { {"苹果","apple"}, {"香蕉","banana"},{"西瓜","watermelon"} };
	// 里层花括号:构造pair<const string,string>类型对象
	// 外层花括号:initializer_list<pair<const string,string>>构造map对象

	for (auto& kv : m)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
}

2. 一些关键字

2.1 auto

自动类型推导,要求变量必须进行显示初始化,编译器将定义对象的类型设置为初始化值的类型。

int main()
{
	auto a = 1;
	//a的类型为int

	auto pa = &a;
	//pa的类型为int*
}

2.2 decltype

关键字decltype将变量的类型声明为表达式指定的类型。

//decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}

int main()
{
	const int x = 1;
	double y = 2.2;
	decltype(x * y) ret; // ret的类型是double
	decltype(&x) p; // p的类型是int*
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;
	F(1, 'a');
	return 0;
}

2.3 nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

这段代码的作用是定义一个通用的空指针常量 NULL,以便在程序中避免使用未初始化的指针或者空指针导致的错误。在 C++中,空指针常量已经被定义为 nullptr,但是在C中没有类似的内置常量,因此需要通过宏定义的方式来实现。


3. 范围for

形如以下形式,可遍历容器v。底层原理是迭代器,语法糖部分已经详细讲解,不再赘述。

vector<int> v = {1,2,3,4,5};
for(auto e:v)
{
	cout << e << endl;
}

4. 右值引用和移动语义(重点)

4.1 左值引用和右值引用

?传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

  • 什么是左值?什么是左值引用?

    左值: 左值是一个表示数据的表达式(如变量名或指针解引用),可对其取地址或赋值,左值(非const)可以出现在赋值符号=的左边。左值定义为const时,不能赋值,但能取地址。

    左值引用: 给左值取别名。

  • 什么是右值?什么是右值引用?

    右值: 右值也是一个数据的表达式。如:字面常量、表达式的临时结果(如:&var、p+1、匿名对象)、函数的返回值(这个返回值不能是左值引用)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

    右值引用: 给右值取别名。

void test_ref()
{
	// 左值引用
	int a = 10;
	int& ra = a;
	const int& rra = a;//权限缩小
	const int& rrra = 10;//权限平移
	/*int& rra = 10;*///err,10是右值

	// 右值引用
	int b = 1;
	/*int&& rb = b;*/ //err,b是左值
	int&& rb = 1;
	
	int&& rrb = move(b);//move将左值转化为右值

	cout << rb << endl;
}

左值引用:一个&

右值引用:两个&

⭕总结:

左值引用不能引用右值,但const左值引用可以引用左值和右值
右值引用不能引用左值,但右值引用可以引用move后的左值


4.2 右值引用的应用

右值一般又可以分为两种

  1. 纯右值:内置类型表达式的临时值,例如常量、字面量、临时对象等。
  2. 将亡值:将亡值是指具有“资源所有权转移”的右值。它是C++11引入的新概念,用于表示即将被销毁的对象的值,但其资源可以被转移到其他对象。 即将被销毁的对象,例如:函数返回对象、匿名对象等等。

右值引用的应用是利用了将亡值的特性支持移动语义。

将亡值的引入主要是为了支持移动语义,通过将资源的所有权转移给新对象,可以避免进行昂贵的拷贝操作,提高效率。这在处理大型对象、容器和动态分配的内存时特别有用。

为了演示移动语义的实现,我们需要先自定义一个string类型(后面称之为ckf::string,因为我将其定义在ckf命名空间中),再写一个to_string用作测试函数

namespace ckf
{
	class string
	{
	public:

		typedef char* iterator;
		
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

		~string()
		{
			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];
				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] = '';
		}

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

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
	
	// 整型转字符串函数
	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());
		
		//上面是对自定义string类型对象str的处理
		//因为str是在该函数域中定义的,出了该作用域就会被销毁
		//因此下面的return str中,str就是将亡值
		
		return str;
	}
}

?试着运行test1函数

void test1()
{
	ckf::string s1 = ckf::to_string(1234);
}

⭕现象,即调用了ckf::string类型的拷贝构造函数

在这里插入图片描述

?现象分析

在这里插入图片描述

test1调用to_string函数后,压入to_string函数栈帧,创建str对象并处理完毕后,to_string向test1返回str到s1的过程是:在压入to_string函数栈帧前先压入了一个临时对象,to_string返回值时,先拷贝构造给临时对象,弹出to_string函数栈帧后,再从临时对象拷贝到test1栈帧中的s1。因此会调用ckf::string的拷贝构造两次。但新一点的编译器都会对这个过程进行优化,省去一次拷贝构造。

在这里插入图片描述

因此,最终调用test1函数时,ckf::string的拷贝构造只被调了一次。

但是拷贝构造毕竟是深拷贝,效率还是比较低,这是传值返回的一个缺陷。前面我们填补这个缺陷的一个办法是将传值返回改为传引用返回,但是这里str出了作用域就销毁了,不能传引用(左值引用)返回。C++11在此基础上又做出了优化,也就是移动语义的引入。 拷贝构造对象时,每次要重新开辟空间,拷贝数据,进行一个深拷贝的过程,这也会降低效率。而移动语义就是省去深拷贝的过程,直接把将亡值的资源所有权转移给新对象,提高效率,称之为移动构造。

?移动构造的大致示意图

在这里插入图片描述

这个过程需要s1是右值才能实现,我们知道临时对象一般被视为右值,因此调用test1,若实现移动语义,则如下:

在这里插入图片描述

编译器再次进行优化。由于对象str在栈帧销毁后被释放,因此编译器视其为右值(将亡值),直接将str的资源所有权转移给s1。

在这里插入图片描述

?移动构造的实现

namespace ckf
{
	class string
	{
	public:
		//...
		// 拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		
		//移动构造
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;
			swap(s);//将亡值的资源与新对象的资源交换即可
		}            
		
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
        
		
	private:
		//...
	}

}

拷贝构造和移动构造,两个函数构成重载。若参数是左值,调用拷贝构造;若参数是右值(将亡值),调用移动构造。

⭕结果,调用了一次移动构造(也称为移动拷贝)

在这里插入图片描述


⭕移动语义不仅可以运用于构造,也可以用于赋值,称为移动赋值。道理和移动构造类似,若等号右边的值是右值,则可以将其资源使用权转移给左边的对象,这就是移动赋值。

写一个test2函数,测试ckf::string类的赋值

void test2()
{
	ckf::string s1;
	s1 = ckf::to_string(1234);//
}

由于赋值需要创建临时变量, 编译器无法优化一步到位,因此在底层有以下过程:

在这里插入图片描述

因此会有两次深拷贝

在这里插入图片描述

最后一次是由于string& operator=(const string& s)函数中拷贝构造了一个tmp对象,用了一次深拷贝。这并不影响赋值的整体过程,可忽略。

?同样的道理,如果ckf::string类实现了移动构造和移动赋值,这两次深拷贝就会变化成两次移动,避免深拷贝带来的效率损耗。

namespace ckf
{
	class string
	{
	public:
		//...
		// 拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

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

⚙再次运行test2,发现拷贝构造变成移动拷贝,赋值变成移动赋值。

在这里插入图片描述

底层的过程变化如下:
在这里插入图片描述

C++11标准库中,为许多类型提供了移动构造和移动赋值函数。

参见:vector类型

在这里插入图片描述

在这里插入图片描述

以上是右值引用移动语义的应用场景之一,将函数返回值视为右值(将亡值),执行移动语义,解决了传值返回的拷贝问题。除此之外,对于插入一些右值数据,也可以减少拷贝。

C++11中,STL容器插入接口函数引入了右值引用的版本,下面以std::list为例分析

**[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0PiAKoDO-1686665395338)(C:UsersCkfAppDataRoamingTypora	ypora-user-imagesimage-20230608222424548.png)]**

void push_back(value_type&& val);

void test3()
{
	list<ckf::string> lt;

	ckf::string s1("11111");
	lt.push_back(s1);//拷贝构造

	lt.push_back("22222");//移动构造
	lt.push_back(ckf::string("33333"));//移动构造
	lt.push_back(move(s1));//移动构造
}

向链表插入节点,若val为左值,则节点的数据域需要拷贝val构造,产生一个新对象。若参数为右值,调用右值引用版本的push_back,节点的数据域接管val的资源使用权,无需进行深拷贝。

在这里插入图片描述


4.3 总结

右值引用和左值引用的本质都是起别名,作用都是减少拷贝,只是原理不太一样。左值引用起别名后,直接用别名发挥作用。而右值引用则是间接起作用,实现移动构造和移动赋值,在拷贝的场景中,如果是右值,转移资源。


5. 万能引用和完美转发

万能引用(Universal Reference) 是一种特殊类型的引用,它可以接受任意类型(包括左值和右值)的引用,并且能够保留被引用对象的值类别(value category)。

void Func(int& x) { cout << "void Func(int& x)" << endl; }

void Func(const int& x) { cout << "void Func(const int& x)" << endl; }

void Func(int&& x) { cout << "void Func(int&& x)" << endl; }

void Func(const int&& x) { cout << "void Func(const int&& x)" << endl; }


// 模板中的万能引用
template<typename T>
void PerfectForward(T&& t)//当传入左值时,会发生引用折叠
{
    // 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
	// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
	// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
	// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
    Func(t);
}

int main()
{
	PerfectForward(10);

	int a = 0;
	PerfectForward(a);//左值
	PerfectForward(move(a));//右值
    
	const int b = 1;
	PerfectForward(b);//const左值
	PerfectForward(move(b));//const右值
    
    retrun 0;
}

上面代码运行结果如下,PerfectForward中的t都退化成左值,调用Func也是传入左值类型。

在这里插入图片描述

?为什么右值引用在后续使用中退化成了左值呢?

因为右值引用设计出来的最大意义就是支持移动语义,即移动构造和移动赋值,这两者都需要转移资源,也就是要修改对象。若右值引用保持右值状态,则具有常性,资源就无法转移出去。所以,右值引用引用的是右值,但其本身是一个左值。

若想要在不丢失值类别的情况下将参数传递给其他函数,应该用到完美转发(perfect forwarding)

下面是一个使用万能引用实现完美转发的示例代码:

template<typename T>
void forwardValue(T&& value)
{
    someFunction(std::forward<T>(value));
}

用完美转发修改前面的代码:

void Func(int& x) { cout << "void Func(int& x)" << endl; }

void Func(const int& x) { cout << "void Func(const int& x)" << endl; }

void Func(int&& x) { cout << "void Func(int&& x)" << endl; }

void Func(const int&& x) { cout << "void Func(const int&& x)" << endl; }

// 万能引用
template<typename T>
void PerfectForward(T&& t)//当传入左值时,会发生引用折叠
{
    //参数t接收时的类型 --> t传入以后的类型
	//右值引用 --> 左值
	//const右值引用 --> const左值,无法被修改,因此尽量不要对常量变量使用move
	Func(forward<T>(t));
}

void test4()
{
	PerfectForward(10);

	int a = 0;
	PerfectForward(a);//左值
	PerfectForward(move(a));//右值

	const int b = 1;
	PerfectForward(b);//const左值
	PerfectForward(move(b));//const右值
}

在这里插入图片描述

⭕总结

使用万能引用的主要用途就是实现完美转发,完美转发在传参的过程中保留对象原生类型属性。


6. 新的类默认成员函数

?原来的c++中,类的默认成员函数有六个

  1. 构造函数
  2. 析构函数
  3. 拷贝构造
  4. 拷贝赋值重载
  5. 取地址重载
  6. const取地址重载

而在C++11中新增了两个:移动构造移动赋值

移动构造函数和移动赋值函数要想默认生成,需要一些特殊条件。

  • 移动构造:==如果一个类没有实现移动构造函数,且没有实现析构函数、拷贝构造函数、拷贝赋值重载函数,那么编译器会自动生成一个默认移动构造函数。==默认生成的移动构造,对于内置类型成员,按字节序拷贝;对于自定义类型成员,则需看它有没有移动构造,若有则调用它的移动构造,若无则调用它的拷贝构造。

  • 移动赋值:==如果一个类没有实现移动赋值函数,且没有实现析构函数、拷贝构造函数、拷贝赋值重载函数,那么编译器会自动生成一个默认移动赋值函数。==默认生成的移动赋值,对于内置类型成员,按字节序拷贝;对于自定义类型成员,则需看它有没有移动赋值,若有则调用它的移动赋值,若无则调用它的拷贝赋值重载。

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

    //析构、拷贝构造、拷贝赋值都不提供,会自动生成默认移动构造和移动赋值
    
	//Person(const Person& p)
	//	:_name(p._name)
	//	, _age(p._age)
	//{}

	//Person& operator=(const Person& p)
	//{
	//	if (this != &p)
	//	{
	//		_name = p._name;
	//		_age = p._age;
	//	}
	//	return *this;
	//}

	//~Person()
	//{}

private:
    //默认移动构造会调用ckf::string的移动构造
    //默认移动赋值会调用ckf::string的移动赋值
	ckf::string _name;
	int _age;
};

int main()
{
	Person p1("张三", 20);
	Person p2(move(p1));
	
	Person p3(Person("李四", 18));
	p2 = move(p3);

	return 0;
}

?执行上面的代码

在这里插入图片描述

注意:如果你提供了移动构造或移动赋值,那么编译器不会自动提供拷贝构造和拷贝赋值。如下面代码

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

	Person(Person&& p)
		:_name(forward<ckf::string>(p._name))//调用ckf::string的移动构造
		,_age(p._age)
	{}

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

int main()
{
    Person p2;
	Person p3(Person("李四", 18));
	Person p4(p3);//err,没有构造函数
	p2 = p3;//err,没有拷贝赋值
	return 0;
}

在这里插入图片描述

?为了更灵活的使用和控制类的默认成员函数,C++11提供了两个新的关键字:defaultdelete(与先前的delete功能不同)

default:强制生成默认成员函数

  • 假设你要使用某个默认的成员函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了移动构造,就不会生成默认拷贝构造函数了,那么我们可以使用default关键字显式指定拷贝构造生成。

?如下代码:

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

	Person(Person&& p)
		:_name(forward<ckf::string>(p._name))//调用ckf::string的移动构造
		,_age(p._age)
	{}
	
    //下面这种声明方式可以强制生成拷贝构造和拷贝赋值重载
	Person(const Person& p) = default;
	Person& operator=(const Person& p) = default;

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

int main()
{
    Person p2;
	Person p3(Person("李四", 18));
	Person p4(p3);
	p2 = p3;
	return 0;
}

刚才出错的代码现在运行成功了,因为Person类强制生成了拷贝构造和拷贝赋值重载。

在这里插入图片描述

delete:强制删除默认成员函数

  • 作用与default相反,delete声明指定删除某个默认成员函数,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本

?如下代码:

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

    //不让默认拷贝构造和拷贝赋值生成
	Person(const Person& p) = delete;
	Person& operator=(const Person& p) = delete;

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

int main()
{
    Person p2;
	Person p3(Person("李四", 18));

	Person p4(p3);//err,拷贝构造已被删除
	p2 = p3;//err,拷贝赋值已被删除

	return 0;
}

7. 可变参数模板

C++11引入了可变参数模板*(variadic templates)*的特性,它允许在模板中使用可变数量的参数。这个特性为函数和类模板提供了更大的灵活性,使得它们可以处理不定数量的参数。

可变参数模板的语法使用了 “…”(省略号)表示可变参数的部分。可以用sizeof...()获取参数包中的参数数目,下面是一个简单的可变参数模板函数的示例:

template <class... Args>    //Args是模板参数包
void ShowList(Args... args) //args是函数参数包
{
    cout << sizeof...(args) << endl;
}

int main()
{
	ShowList(1, 'x', string("hello"), 3.14);
	return 0;
}

执行结果:

4

⭕参数包的展开有两种方法:

  1. 递归展开法

    //递归展开参数包
    template <class T>
    void ShowList(const T& val)//终止函数
    {
    	cout << val;
    }
    
    // Args.../args... ==> 在参数包后面加省略号,因为实际运用时要拓展参数包
    template <class T, class... Args>
    void ShowList(const T& val, Args... args)
    {
    	cout << val << " ";
    
    	ShowList(args...);
    }
    
    
    int main()
    {
    	ShowList(1, 'x', string("hello"), 3.14);
    	return 0;
    }
    

    运行结果

    1 x hello 3.14

    ?递归展开图

在这里插入图片描述

  1. 逗号表达式展开参数包

    template <class T>
    void show(const T& val)
    {
    	cout << val << " ";
    }
    
    template <class... Args>
    void ShowList(Args... args)
    {
    	int arr[] = { (show(args),0)... };
    	// 展开为:
    	// int arr[] = {(show(arg1),0),(show(arg2),0),(show(arg3),0)...};
    	// 数组arr的内容最终全为0,括号表达式的主要目的是为了让前半部分执行
    }
    
    
    int main()
    {
    	ShowList(1, 'x', string("hello"), 3.14);
    	return 0;
    }
    

    运行结果

    1 x hello 3.14

?C++11为STL容器提供了emplace系列接口,支持可变参数模板,并且是万能引用。

截取文档中的一部分来介绍。

std::list::emplace_back

template <class... Args>
  void emplace_back (Args&&... args);

Construct and insert element at the end

Inserts a new element at the end of the list, right after its current last element. This new element is constructed in place using args as the arguments for its construction.

emplace_back和push_back作用一样,是向容器插入元素。有所不同的是emplace_back支持可变参数模板,并且用参数包中的参数作为新插入元素构造函数的参数。

int main()
{
	list<pair<int, ckf::string>> lt;

	lt.push_back(make_pair(1, ckf::string("hello")));//先构造pair对象,pair对象移动构造临时对象,临时对象再移动构造插入的元素,两次移动构造

	lt.emplace_back(1, ckf::string("hello"));//直接构造临时对象,再插入,一次移动构造

	return 0;
}

在这里插入图片描述

emplace在一些场景下会比push效率快一点,但是差别不大。


8. lambda表达式

8.1 引入

场景:在学生管理系统中,按照不同指标为学生排序

?传统的C++可能会这样写:调用算法库的sort函数,并分别写出按年龄、按成绩、按名字比较的仿函数,传入sort进行排序。

struct Student
{
	int _age;//年龄
	double _score;//分数
	string _name;//名字
};

struct cmpByAge
{
	bool operator()(const Student& s1, const Student& s2) { return s1._age < s2._age; }
};

struct cmpByScore
{
	bool operator()(const Student& s1, const Student& s2) { return s1._score < s2._score; }
};

struct cmpByName
{
	bool operator()(const Student& s1, const Student& s2) { return s1._name < s2._name; }
};


int main()
{
	vector<Student> v = { {18,91.7,"Mike"},{20,89.4,"Joe"},{19,96.0,"David"} };

	sort(v.begin(), v.end(), cmpByAge());// 按年龄排序
	sort(v.begin(), v.end(), cmpByScore());// 按成绩排序
	sort(v.begin(), v.end(), cmpByName());// 按名字排序

	return 0;
}

但是这样的代码有以下的缺点:

  • 代码冗余,每个用于比较的仿函数函数只有在对应的排序函数下使用
  • 每多一种排序方式,就要多一个仿函数,且逻辑都不同
  • 仿函数太多,管理难度高,调用的时候可能会不知道调用哪一个(像上面一样给函数命名可以稍微避免)。

为此,C++11提供了lambda表达式,

int main()
{
	vector<Student> v = { {18,91.7,"Mike"},{20,89.4,"Joe"},{19,96.0,"David"} };

	sort(v.begin(), v.end(), [](const Student& s1, const Student& s2) { return s1._age < s2._age; });// 按年龄排序
	sort(v.begin(), v.end(), [](const Student & s1, const Student & s2) { return s1._score < s2._score; });// 按成绩排序
	sort(v.begin(), v.end(), [](const Student & s1, const Student & s2) { return s1._name < s2._name; });// 按名字排序

	return 0;
}

?上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名仿函数

8.2 lambda表达式的格式

?lambda表达式的格式:

[capture-list](parameters)mutable->return-type{statement}

  • capture-list:捕捉列表。该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。(不可省略)
  • paremeters:参数列表。与普通函数的参数列表相同。如果没有参数传递,则可连同括号省略。
  • mutable:取消常性。按值捕获到lambda函数中的变量在函数体中默认是const类型,即不可修改,在添加了mutable修饰符后,便可以对此变量进行修改,但此时仍然修改的是位于lambda函数体中的局部变量
  • return-type:返回值类型。返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
  • statement:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

?捕捉列表的说明

捕捉列表描述了上下文中哪些数据可以为lambda表达式所用,分为传值捕捉和传引用捕捉

  • [var]:表示传值捕捉父作用域的变量var
  • [=]:表示传值捕捉父作用域的所有变量(包括this指针)
  • [this]:表示传值捕捉当前的this指针
  • [&var]:表示传引用捕捉父作用域的变量var
  • [&]:表示传引用捕捉父作用域的所有变量(包括this指针)

⭕注意:捕捉的对象必须在父作用域中,且在lambda表达式的上文

int main()
{
	//最简单的lambda表达式
	[] {};

	//省略了参数列表和返回值类型,传值捕捉a
	int a = 1;
	[a] {return a + 10; };

	//返回值类型为void,传引用捕捉,改变b
	int b = 2;
	auto func1 = [&](int c) {b = c; };
	func1(0);

	// 各部分都很完善的lambda函数
	auto func2 = [=, &b](int c)mutable->int {return b += a + c; };
	
    //auto func3 = [a]{return a++; };//err,,传值捕获的a有常性,不能修改
	auto func3 = [a]() mutable{return a++; };//yes
	func3();
    
	return 0;
}

由以上代码可见,lambda表达式不能直接调用,它是一个匿名函数对象,需要用auto类型的变量接收,并调用。

注意:

  • 父作用域指包含lambda函数的语句块语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:

[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;

[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

  • 捕捉列表不允许变量重复传递,否则就会导致编译错误。
    比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

  • 在块作用域以外的lambda函数捕捉列表必须为空。

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

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

在这里插入图片描述

8.3 lambda的底层原理

事实上,编译器底层将lambda表达式视为一个仿函数(函数对象 )。

struct Add
{
	int operator()(int x, int y) const
	{
		return x + y;
	}
};

int main()
{
	Add fun1;
	fun1(1, 2);

	auto fun2 = [](int x, int y) {return x + y; };
	fun2(1, 2);

	return 0;
}

⭕底层都是调用仿函数的operator()


在这里插入图片描述

9. 包装器

?观察下面这个简单的语句

ret = func();

func是什么呢?有可能是函数名、函数指针,可能是函数对象(仿函数),也有可能是lambda表达式对象。它可以是任何可以调用的对象。如此丰富的类型往往会导致模板的效率低下,看下面的代码:

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

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

这里因为传入useF的第一个参数类型不同,模板会推演实例化出三个不同的函数,效率低。

在这里插入图片描述

C++标准库提供了一个名为std::function的函数包装器类模板,定义在<functional>头文件中。std::function可以用于封装各种可调用实体,包括普通函数、函数指针、成员函数指针、函数对象、lambda表达式等。

包装器(function wrapper)的声明如下:

在这里插入图片描述

使用std::function模板必须指明返回值类型和参数列表

  • Ret:返回值类型

  • Args:参数包

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


int main()
{
    //包装器function的使用
	function<double(double i)> f1 = f;
	function<double(double i)> f2 = Functor();
	function<double(double i)> f3 = [](double d)->double { return d / 4; };
	
    //此处便统一了三个可调用实体的类型,使得模板只实例化一个函数,提高效率
	cout << useF(f1, 11.11) << endl;
	cout << useF(f2, 11.11) << endl;
	cout << useF(f3, 11.11) << endl;

	return 0;
}

在这里插入图片描述

bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作

struct add
{
	int operator()(const int& left, const int& right)
	{
		return left + right;
	}
};

int sub(int left, int right)
{
	return left - right;
}

class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b;
	}
};

int main()
{

	//给sub函数的参数列表绑定两个参数,分别是1和2,这样f1就是返回类型为int,无参数的调用实体
	auto f1 = std::bind(sub, 1, 2);
	cout << f1() << endl;

	//placeholders::_n表示调用实体的第n个参数
	auto f2 = std::bind(add(), 1, placeholders::_1);
	cout << f2(4) << endl;

	auto f3 = std::bind(sub, placeholders::_1, placeholders::_2);
	cout << f3(5, 3) << endl;

	//f4相当于调换两个参数位置的sub
	auto f4 = std::bind(sub, placeholders::_2, placeholders::_1);
	cout << f4(5, 3) << endl;

	//绑定成员函数,要传入对象,编译器才能知道是用哪个对象调用的成员函数
	Sub s;
	std::function<int(int, int)> f5 = std::bind(&Sub::sub, s, placeholders::_1, placeholders::_2);
	cout << f5(10,2) << endl;

	return 0;
}

⭕运行结果
在这里插入图片描述


?本文完。如果这篇文章对你有帮助,动动小手点赞收藏加关注支持博主!你的支持是我最大的动力

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。