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

news2024/11/26 2:37:41

文章目录

  • C++11介绍
  • 1. 统一的列表初始化
    • 1.1 {}初始化
    • 1.2 std::initializer_list
  • 2. 一些关键字
    • 2.1 auto
    • 2.2 decltype
    • 2.3 nullptr
  • 3. 范围for
  • 4. 右值引用和移动语义(重点)
    • 4.1 左值引用和右值引用
    • 4.2 右值引用的应用
    • 4.3 总结
  • 5. 万能引用和完美转发
  • 6. 新的类默认成员函数
  • 7. 可变参数模板
  • 8. lambda表达式
    • 8.1 引入
    • 8.2 lambda表达式的格式
    • 8.3 lambda的底层原理
  • 9. 包装器
    • bind


  • 📝 个人主页 :超人不会飞)
  • 📑 本文收录专栏:《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文档介绍

在这里插入图片描述

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] = '\0';
		}

		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:\Users\Ckf\AppData\Roaming\Typora\typora-user-images\image-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;
}

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


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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql小表驱动大表

摘要&#xff1a; 小表驱动大表为了减少匹配时的数据量 判断谁做小表时&#xff0c;是比较算上过滤条件后的数量 left join时手动设置小表驱动大表 inner join时优化器会自动小表驱动大表 course–100条数据 student_info–100w条数据 优化器会选择小表驱动大表&#xff08;这里…

使用VMware Workstation一步一步安装Rocky Linux 9

目录 目录 背景 准备阶段 新建虚拟机 安装Rocky Linux 进入系统 背景 Rocky Linux 简介 企业Linux&#xff0c;社区方式。 Rocky Linux是一个开源的企业操作系统&#xff0c;旨在与红帽企业Linux100%兼容。社区正在大力发展。 Rocky Linux 9.2 于2023年5月16日发布&a…

计算机组成原理(六)指令系统

一、指令的基本格式 1.1机器指令的相关概念 指令集(Instruction Set) 某机器所有机器指令的集合 *定长指令集 指令集中的所有指令长度均相同!取指令控制简单*不定长指令集 指令集中的所有指令长度有长、有短 操作码 (1)长度固定 用于指令字长较长的情况RISC 如IBM370操作码8位…

第四章 Linux网络编程 4.1 网络结构模式 4.2MAC地址、IP地址、端口

第四章 Linux网络编程 4.1 网络结构模式 C/S结构 简介 服务器 - 客户机&#xff0c;即 Client - Server&#xff08;C/S&#xff09;结构。C/S 结构通常采取两层结构。服务器负责数据的管理&#xff0c;客户机负责完成与用户的交互任务。客户机是因特网上访问别人信息的机器…

Ubuntu16.04.7+Qt15.5.0环境配置(一条龙讲解)

目录 1、下载并安装Ubuntu 2、Qt下载与安装 3、Qt环境配置 4、设置编译套件 5、创建qt快速启动脚本 1、下载并安装Ubuntu Ubuntu16.04.7下载链接https://releases.ubuntu.com/xenial/ 安装步骤省略。 2、Qt下载与安装 在Qt5.15之后的版本&#xff0c;官方都不提供离线安装…

Allegro因为精度问题导致走线连接不上的解决办法

Allegro因为精度问题导致走线连接不上的解决办法 在用Allegro做PCB设计的时候,尤其是从其它单板上导数据过来的时候,有时会因为精度不一致导致连接不上,如下图 线和过孔因为精度有微小的连接偏差 一般来说,可以逐个重新连接一下,但是如果连接点位比较多的话,需要花费较多…

在windos中同时使用gitee与github

1.为什么这样做&#xff1f; 原因非常简单&#xff0c;我们遇到自己喜欢的git仓库后&#xff0c;通常会将他们克隆到我们本地电脑上&#xff0c;但这个时候会有一个问题&#xff0c;就是我们喜欢的仓库有可能是gitee仓库&#xff0c;也有可能是github仓库&#xff0c;这个时候…

Windows YOLO v8训练自己的数据集

YOLO v8 训练自己的数据集 环境准备YOLO v8创建自己的数据集1.首先准备了VOC 格式的数据集2.然后确定用于训练、测试的数据3.将VOC格式标注转为YOLO 标注4.配置数据文件 yaml 配置 YOLO v8安装和训练安装依赖包训练 环境准备 这里我的环境是Windows 环境 YOLO v8 下载链接&a…

dma-fence使用demo

dma-fence是内核中一种比较常用的同步机制,本身的实现和使用并不复杂&#xff0c;其只有两种状态signaled和unsignaled。可能正是因为其本身的精简&#xff0c;在融入其他概念中时&#xff0c;在不同的环境下&#xff0c;赋予了dma-fence不同的含义。所以通常需要根据dma-fence…

DragGAN论文阅读

文章目录 摘要问题3. 算法&#xff1a;3.1 基于点的交互式操作3.2 运动监督3.3 点跟踪 4. 实验4.1 质量评估4.2 量化评估4.3 讨论 结论 论文&#xff1a; 《Drag Your GAN: Interactive Point-based Manipulation on the Generative Image Manifold》 github&#xff1a; htt…

一文学会Git管理代码仓库

文章目录 一、预备知识1.Linux常用指令2.vim编辑器基本使用 二、Git基础1.工作区、暂存区、本地仓库和远程仓库2.git init3.git add4.git status5.git commit6.git push7.git pull8.git 分支管理(branch、checkout、merge)9.git clone和log10.git diff11.git fetch12.git rm13.…

汽车IVI中控开发入门及进阶(九):显示屏

前言: 显示屏Display panel和触控屏Touch panel,可以说随着汽车四化的进展,屏越来越多,越来越大,越来越高清,成为IVI中控、智能座舱系统的重要组成部分。比如如下一个电阻触摸屏。 正文: 显示屏 主要功能就是显示,车载内容和信息的传递全靠显示屏,目前车载显示屏的…

Java企业级开发学习笔记(4.4)Spring Boot加载自定义配置文件

一、使用PropertySource加载自定义配置文件 1.1 创建Spring Boot项目 创建Spring Boot项目 单击【创建】按钮 1.2 创建自定义配置文件 在resources里创建myconfig.properties文件 设置文件编码 设置学生的四个属性值 1.3 创建自定义配置类 在cn.kox.boot包里创建confi…

毕业季Android开发面试,有哪些常见的题?

前言 对于计算机行业早已烂大街&#xff0c;随之而来的毕业季。还会有大批的程序员涌进来&#xff0c;而我们想要继续进入Android开发岗位的人员&#xff0c;最先考虑的是面试。面试题是我们决定踏进工作的重要环节。 对于刚毕业的实习生来说&#xff0c;如何在应聘中脱颖而出…

uniapp 使用app-plus自定义导航栏(titleNView)右侧自定义图标+文字按钮

前言 最近在使用uni-app写H5移动端时候&#xff0c;在pages.json文件配置导航栏的图标时发现不生效 去官网查阅了很久&#xff0c;意思是不支持本地图片&#xff0c;支持iconfont.ttf&#xff08;iconfont本地下载&#xff09;格式图片 意思是在导航栏的图标需要在iconfont图…

Synchronized 偏向锁、轻量级锁、自旋锁、锁消除

一、重量级锁 上篇文章中向大家介绍了Synchronized的用法及其实现的原理。现在我们应该知道&#xff0c;Synchronized是通过对象内部的一个叫做监视器锁&#xff08;monitor&#xff09;来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实…

【从Spring Cloud到Spring Cloud Alibaba,这些改变你都知道吗?】—— 每天一点小知识

&#x1f4a7; 从 S p r i n g C l o u d 到 S p r i n g C l o u d A l i b a b a &#xff0c;这些改变你都知道吗&#xff1f; \color{#FF1493}{从Spring Cloud到Spring Cloud Alibaba&#xff0c;这些改变你都知道吗&#xff1f;} 从SpringCloud到SpringCloudAlibaba&#…

【LeetCode训练营 189】轮转数组详解

&#x1f4af; 博客内容&#xff1a;【LeetCode训练营 189】轮转数组详解 &#x1f600; 作  者&#xff1a;陈大大陈 &#x1f680; 个人简介&#xff1a;一个正在努力学技术的准前端&#xff0c;专注基础和实战分享 &#xff0c;欢迎私信&#xff01; &#x1f496; 欢迎大…

Tensorflow训练代码1.x接口自动升级2.x踩坑记录

Tensorflow训练代码1.x接口自动升级2.x踩坑记录 TF准备工作环境问题解决自动升级脚本&#xff0c;从TF1.0调通到TF2.0 一起学AI系列博客&#xff1a;目录索引 本文小结Tensorflow训练代码1.x接口自动升级2.x踩坑过程和问题解决的方法。 TF准备工作 Tensorflow环境准备 前提已…

【MySQL高级篇笔记-多版本并发控制MVCC(下) 】

此笔记为尚硅谷MySQL高级篇部分内容 目录 一、什么是MVCC 二、快照读与当前读 1、快照读 2、当前读 三、复习 1、再谈隔离级别 2、隐藏字段、Undo Log版本链 四、MVCC实现原理之ReadView 1、什么是ReadView 2、设计思路 3、ReadView的规则 4、MVCC整体操作流程…