C++11

C++11

C++11

1. C++发展史

1982年,Bjarne Stroustrup 博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,所以将其命名为C++。简言之,C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。C++ 的发展史如下:

版本内容
C with classes类及派生类、公有和私有成员、类的构造和析构、友元、内联函数、赋值运算符重载等
C++1.0添加函数概念、函数和运算符重载、引用、常量等
C++2.0更加完善面向对象功能,新增加了继承、多重继承、对象的动态化初始化,抽象类,静态成员及const成员函数
C++3.0进入小型语言,引入模板,解决多重重载产生的二义性问题和继承模式的处理
C++98C++标准第一版,绝大多数编译器都支持,得到了国际标准化组织(ISO)和美国国家标准协会认可,可以开始支持标准C++标准库,引入了STL(标准模板库)
C++03C++标准第二个版本,语言特性无太大变化,主要:修订错误、减少异性性
C++05C++标准委员会发布了一份技术报告(Technical Report, TR1),正式提出C++0x,计划在本世纪纪年第一十年的某个时刻发布
C++11增加了许多新特性,使得C++更像一种现代语言,比如:正则表达式、基于范围的for循环、auto关键字、新容器、列表初始化、右值引用、标准线程等
C++14对C++11的扩展,主要修复C++11中的语法问题以及改进,比如:泛型编程lambda表达式、auto内联的值类型推导、二进制和常规数字字面量
C++17在C++11做了一些小幅度修改,增加了9个新特性,比如:static_assert()的文本信息可用,Fold表达式可用于元组的递归,和switch语句中的强力保障
C++20自C++11以来最大版本,引入了许多新的特性,比如:模块(Modules)、协程(Coroutines)、范围(Ranges)、概念(Constraints)等重要特性,还有自定义的特性;例如Lambda支持静态模式等
C++23制定ing

2. 列表初始化

2.1 统一使用 {} 初始化

2.1.1 C++98传统的{}

C++98 中一般数组和结构体可以用 {} 进行初始化。

struct Point 
{
	int _x;
	int _y;
};

int main() 
{
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };
    
	return 0;
}
2.1.2 C++11中的{}
  • C++11 以后统一初始化方式,试图实现一切对象皆可用 {} 初始化,{} 初始化也叫做列表初始化

  • 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,其中会产生临时对象,最后优化以后变成直接构造。

  • {} 初始化的过程中,可以省略括号 =。

  • C++11 列表初始化的本意是想实现一个大统一的初始化方式,其次它在有些场景下带来的不少便利,如容器 push/inset 多参构造的对象时,{} 初始化会很方便。

// 示例
#include<iostream>
#include<vector>
using namespace std;

// 结构体
struct Point
{
	int _x;
	int _y;
};

// 自定义类型
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

// ⼀切皆可⽤列表初始化,且可以不加= 
int main()
{
    // C++98支持的 
    int a1[] = { 1, 2, 3, 4, 5 };
    int a2[5] = { 0 };
    Point p = { 1, 2 };

    // C++11支持的 
    // 内置类型支持 
    int x1 = { 2 };

    // 自定义类型支持 
    // 这里本质是用{ 2025, 1, 1 }构造一个Date临时对象 
    // 临时对象再去拷贝构造d1,编译器优化后合二为一变成{ 2025, 1, 1 }直接构造初始化d1
    // 本质都是由构造函数支持的隐式类型转换
    Date d0 = 2020;	// 单参数初始化,走的是单参数隐式类型转换
    Date d1 = { 2025, 1, 1 };

    // 这里d2引用的是{ 2024, 7, 25 }构造的临时对象 
    const Date& d2 = { 2024, 7, 25 };

    // 需要注意的是C++98支持单参数时类型转换,也可以不用{} 
    Date d3 = { 2025 };
    Date d4 = 2025;

    // 可以省略掉= 
    Point p1{ 1, 2 };
    int x2{ 2 };
    Date d6{ 2024, 7, 25 };
    const Date& d7{ 2024, 7, 25 };
    // 不支持,只有{}初始化,才能省略= 
    // Date d8 2025;

    vector<Date> v;
    v.push_back(d1);
    v.push_back(Date(2025, 1, 1));

    // 比起有名对象和匿名对象传参,这里{}更有性价比 
    v.push_back({ 2025, 1, 1 });

    return 0;
}

补充:

  • d0 、d1 和 d2 的初始化方式

    这里的 Date 类型由于每个参数都有缺省值,也就支持单参数和多参数类型转化。

    所以这里的 d0 使用单参数初始化,走的是单参数隐式类型转换,而 d1 使用多个参数初始化,走的是多参数隐式类型转换,但是注意多参数隐式类型转换的参数需要使用 {} 括起来。

    这里的初始化本质或者说是语法规则都是通过右边的参数先临时构造一个左边同类型的临时对象,再通过拷贝构造初始化左边的目标对象。但是连续的构造和拷贝构造编译器会认为造成了资源的浪费,所以编译器直接优化成了直接构造

    因为以上的初始化本质所以引出了 d2 的初始化方式,这里的引用本质是引用 { 2024, 7, 25 } 构造的临时对象。并且临时对象具有常性需要使用 const 修饰。

2.2 initializer_list 类

initializer_list 是 C++11 中新增的一个类,其文档介绍如下:initializer_list - C++ Reference (cplusplus.com)

在这里插入图片描述

它可以将同一类型元素的集合即由相同元素构成的一个列表转化为一个 initializer_list 的对象。需要注意的是,initializer_list 实际上是对常量区的封装,将列表中的数据识别为常量区的数据,然后用 initializer_list 提供的两个类似于迭代器的 begin() 和 end() 成员函数用于指向并访问这些数据,其自身并不会开辟空间,所以 initializer_list 中的数据也不能修改。

int main()
{
	std::initializer_list<int> mylist;
	mylist = { 10, 20, 30 };
	cout << sizeof(mylist) << endl;

	// 这里begin和end返回的值initializer_list对象中存的两个指针 
	// 这两个指针的值跟i的地址跟接近,说明mylist存在栈上 
	int i = 0;
	cout << mylist.begin() << endl;
	cout << mylist.end() << endl;
	cout << &i << endl;
}

有了 initializer_list 类以后,就可以让 STL 的其他容器重载一个参数为 initializer_list 类型的构造函数和赋值函数,从而使得这些容器支持使用列表来进行初始化和赋值
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

int main()
{
    // {}列表中可以有任意多个值 
 	// 这两个写法语义上还是有差别的,第⼀个v1是直接构造,  
 	vector<int> v1({ 1,2,3,4,5 });
    // 第⼆个v2是构造临时对象+临时对象拷⻉构造v2,编译器优化为直接构造
 	vector<int> v2 = { 1,2,3,4,5 };
    // 第三个v3是引用构造的临时对象
 	const vector<int>& v3 = { 1,2,3,4,5 };
    
 	// 这⾥是pair对象的隐式类型转换进行初始化和map的initializer_list构造结合到⼀起⽤了 
 	map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"}};
 
 	// initializer_list版本的赋值⽀持 
 	v1 = { 10,20,30,40,50 };
 	
    return 0;
}

3. 右值引用和移动语义

3.1 左值和右值

  • 左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持续状态,存储在内存中,可以获取它的地址,左值可以出现在赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰后的左值,不能给他赋值,但是可以取它的地址
int main()
{
 	// 左值:可以取地址 
 	// 以下的p、b、c、*p、s、s[0]就是常⻅的左值 
 	int* p = new int(0);
 	int b = 1;
 	const int c = b;
 	*p = 10;
 	string s("111111");
 	s[0] = 'x';
    
 	cout << &c << endl;
 	cout << (void*)&s[0] << endl;
}

补充:

这里的 &s[0] 是char* 指针,char* 指针在打印的时候不会按照地址形式打印,因为C/C++中有字符和字符串的概念,会按照字符串内容进行打印,这里也就是打印 x11111 ,所以如果想在这里打印出地址需要强转为 void*在打印即可。

  • 右值也是一个表示数据的表达式,要么是临时值,要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址
int main()
{
    // 右值:不能取地址 
 	double x = 1.1, y = 2.2;
 	// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值 
 	10;
 	x + y;			// 表达式结果使用临时对象存储
 	fmin(x, y);		// 传值返回的函数调用其结果使用临时对象存储
 	string("11111");// 匿名对象
}
  • 值得一提的是,左值的英文写法 lvalue,右值的英文写法 rvalue。传统认为它们分别是 left valueright value 的缩写。现代 C++ 中,lvalue 被解释为 locator value 的缩写,可以认为存储在内存中,有确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可寻址,例如:临时变量、字面量常量、存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址

3.2 左值引用和右值引用

  • 特点一:

    Type& r1 = xType&& r1r = y

    第一个语句是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。

    int main()
    {
     	// 左值:可以取地址 
     	// 以下的p、b、c、*p、s、s[0]就是常⻅的左值 
     	int* p = new int(0);
     	int b = 1;
     	const int c = b;
     	*p = 10;
     	string s("111111");
     	s[0] = 'x';
     	double x = 1.1, y = 2.2;
        
     	// 左值引⽤给左值取别名 
     	int& r1 = b;
     	int*& r2 = p;
     	int& r3 = *p;
     	string& r4 = s;
     	char& r5 = s[0];
        
     	// 右值引⽤给右值取别名 
        int&& rr1 = 10;
     	double&& rr2 = x + y;
     	double&& rr3 = fmin(x, y);
     	string&& rr4 = string("11111");
    }
    
  • 特点二:

    左值引用不能直接引用右值,但是const左值引用可以引用右值

    右值引用不能直接引用左值,但是右值引用可以引用move(左值)

    int main()
    {
    	// 左值引用不能直接引用右值,但是const左值引用可以引用右值 
    	const int& rx1 = 10;
    	const double& rx2 = x + y;
    	const double& rx3 = fmin(x, y);
    	const string& rx4 = string("11111");
    
    	// 右值引用不能直接引用左值,但是右值引用可以引用move(左值) (本质就是强转所以直接强转再引用也可以)
    	int&& rrx1 = move(b);
    	int*&& rrx2 = move(p);
    	int&& rrx3 = move(*p);
    	string&& rrx4 = move(s);
    	string&& rrx5 = (string&&)s;
    }
    

    补充:move()

    • template <class T> typename remove_reference<T>::type&& move (T&& arg);

      move是库里面的一个函数模板,本质内部是进行强类型转换,当然他还涉及一些引用折叠的知识,这个后面会详细介绍。

  • 特点三:

    需要注意的是变量表达式都是左值,也意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值

    这样设计的目的是为了允许后续的移动构造和移动赋值中的被右值引用绑定的右值交换资源。因为右值是没有办法被修改的,只有将其设定为左值才可以被修改,实现资源的交换。

    int main()
    {
    	// b、r1、rr1都是变量表达式,都是左值 
    	cout << &b << endl;
    	cout << &r1 << endl;
    	cout << &rr1 << endl;
    }
    
  • 特点四:

    语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编层面的角度看下面代码中r1和r1r汇编层实现,底层都是用指针实现的,没有什么区别。

    底层汇编实现和上层语法表达的意义有时是背离的,所以不要盲目一起去理解,互相纠缠,这样反而会陷入迷途

3.3 引用延长生命周期

右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长对象生命周期,但这些对象无法被修改。

int main()
{
    // 示例1:
    std::string s1 = "Test";
    std::string&& r1 = s1;           	// 错误:不能绑定到左值

    // 示例2:
    const std::string& r2 = s1 + s1;    // OK:到 const 的左值引用延长生命周期
    r2 += "Test";                    	// 错误:不能通过 const 的引用修改

    // 示例3:
    std::string&& r3 = s1 + s1;         // OK:右值引用延长生命周期
    r3 += "Test";                       // OK:能够通过非 const 的引用修改

    std::cout << r3 << '\n';

    return 0;
}

补充:

  • 示例1:

    这里的 s1 是左值,无法被一个右值引用 r1 绑定。

  • 示例2:

    因为 s1 是左值,相加后将结果存在一个临时对象中,但是因为匿名对象和临时对象的生命周期都只在当前这一行,但是经过了 const + 左值引用之后其生命周期延长到和 r2一样。

  • 示例3:

    这里是通过右值引用延长临时对象的生命周期,并且因为右值引用没有 const 修饰这里是可以被修改的。

总结: 这里虽然使用右值引用和 const + 左值引用都可以达到延长对象生命周期的目的,但是二者的使用场景并不相同,这个相关知识点后续进行介绍。

3.4 左值和右值的参数匹配

  • C++98中,实现一个 const + 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
  • C++11以后,分别重载左值引用const左值引用右值引用作为参数的函数,那么实参是左值会匹配(const左值引用),实参是右值会匹配(右值引用)
  • 右值引用变量用于表达式属性是左值,这个设计里会感觉跟常规使用场景时,就能体会这样设计的价值。
void f(int& x)
{
 	std::cout << "左值引⽤重载 f(" << x << ")\n";
}

void f(const int& x)
{
 	std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}

void f(int&& x)
{
 	std::cout << "右值引用重载 f(" << x << ")\n";
}


int main()
{
    int i = 1;
    const int ci = 2;

    f(i);    // 调用 f(int&)
    f(ci);   // 调用 f(const int&)
    f(3);    // 调用 f(int&&),如果没有 f(int&&) 重载则调用 f(const int&)
    f(std::move(i));  // 调用 f(int&&)
    
    // 右值引用变量在用于表达式时是左值 
    int&& x = 1;
 	f(x); // 调用 f(int& x) 
 	f(std::move(x)); // 调用 f(int&& x)
    
    return 0;
}

其实这种设计的本质还是参数会优先找现成的和自己类型一样的构造函数,如果没有现成的构造函数就找和自己参数类型最适配的。

3.5 右值引用和移动语义的使用场景

3.5.1 左值引用主要使用场景回顾

使用场景:左值引用主要使用场景是在函数中左值引用传参左值引用传值减少拷贝,同时还可以修改实际传值的返回对象的值

**现存缺陷:**左值引用已经解决大多数场景的拷贝效率问题,但是在某些场景不能使用传值引用返回,如 addStrings 和 generate 函数,C++98 的解决方案可能是被迫使用输入类型参数解决。那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回回对象是一个局部对象,函数结束这个对象就会析构了,右值引用返回也无法概念对象已经析构的事实。

具体代码:场景一

// 用于实现两字符串相加的代码
class Solution 
{
public:
	// 传值返回需要拷贝
	string addStrings(string num1, string num2) 
	{
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		// 进位 
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}
		if (next == 1)
			str += '1';
		reverse(str.begin(), str.end());
		return str;
	}
};

分析:

在这里实现两个字符串相加的代码中可以看出左值引用做返回值的缺陷,这里的 str 是要返回的最终结果,本意如果可以使用传引用返回可以节省空间,但是因为 str 是局部对象,起生命周期只存在于 addStrings 这个函数中,出了这个函数的作用域就会销毁,如果这时还是用传引用返回就会出现问题,所以只可以使用传值返回,但是如果这里的 str 很大,使用传值返回就会浪费大量空间。

具体代码:场景二

// 杨辉三角
class Solution 
{
public:
	// 这里的传值返回考拷贝代价就太⼤了 
	vector<vector<int>> generate(int numRows) 
	{
		vector<vector<int>> vv(numRows);
		for (int i = 0; i < numRows; ++i)
		{
			vv[i].resize(i + 1, 1);
		}
		for (int i = 2; i < numRows; ++i)
		{
			for (int j = 1; j < i; ++j)
			{
				vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
			}
		}
		return vv;
	}
};

分析:

场景二和场景一的情况都是一样的,都是体现出左值引用的缺陷。

以上通过两个左值引用的场景展示了左值引用的局限性,所以又设计了右值引用来适配这种使用场景,但是这里也并不是简单的将返回值类型转为右值引用,具体介绍请继续阅读。

3.5.2 移动构造和移动赋值

对于像 string/vector 这样的深拷贝的类或者包含深拷贝成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用对象的资源,而不是像拷贝构造和拷贝赋值那样拷贝资源,从而提高效率。

3.5.2.1 移动构造

概念:移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类型的引用,但不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。

**本质:**移动构造本质就是将传入右值的资源窃取过来,占为己有,这样就避免了进行深拷贝,所以它叫做移动构造,就是窃取别人的资源来构造自己的意思。

示例:

在当前的 string 类中增加一个移动构造函数,该函数要做的就是调用 swap 函数将传入右值的资源窃取过来,为了能够更好的得知移动构造函数是否被调用,可以在该函数当中打印一条提示语句。

namespace tcq
{
	class string
	{
	public:
        // 构造函数
        string(const char* str = "")
 			:_size(strlen(str))
 			, _capacity(_size)
 		{
 			cout << "string(char* str)-构造" << endl;
 			_str = new char[_capacity + 1];
 			strcpy(_str, str);
 		}
        
 		void swap(string& s)
 		{
 			::swap(_str, s._str);
 			::swap(_size, s._size);
 			::swap(_capacity, s._capacity);
 		}
        
        // 拷贝构造
 		string(const string& s)
 			:_str(nullptr)
 		{
 			cout << "string(const string& s) -- 拷⻉构造" << endl;
            reserve(s._capacity);
 			for (auto ch : s)
 			{
 				push_back(ch);
 			}
 		}

		//移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
        
        // 析构函数
        ~string()
 		{
 			cout << "~string() -- 析构" << endl;
 			delete[] _str;
 			_str = nullptr;
 		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

int main()
{
    tcq::string s1("xxxxx");
	// 拷⻉构造 
 	tcq::string s2 = s1;
 	// 构造+移动构造,优化后直接构造 
 	tcq::string s3 = tcq::string("yyyyy");
 	// 移动构造 
 	tcq::string s4 = move(s1);
 
 	return 0;
}          

分析:

在以上代码中构造 s3 和 s4 中涉及移动构造的概念,主要对 s3 的初始化进行分析。

针对66行 s2 的构造,首先注释掉移动构造的代码,通过直接构造构造出一个匿名对象 tcq::string("yyyyy") 但是因为匿名对象的生命周期只有一行,这里要求利用这个匿名对象初始化 s3 ,按照之前的知识会说让这个匿名对象因为参数没有相同的从而选择更适配的拷贝构造来初始化 s3,在初始化完成之后再释放掉 s2 这块匿名对象的空间这样的一个过程。但是在这个过程中是在一定程度上造成的空间资源的浪费。

而如果有移动构造,在已知这里的 s3 是一个匿名对象是一个右值的情况下,构造会优先的走现成的移动构造,而移动构造的行为是将 s3 这块即将要被释放掉的空间,直接掠夺给 s2,从而完成初始化。

移动构造函数和拷贝构造的区别:

  • 在没有增加移动构造之前,由于拷贝构造采用的是 const 左值引用接收参数,因此元论拷贝构造对对象传入的是左值时都会调用拷贝构造函数。
  • 增加移动构造之后,由于移动构造采用的是右值引用接收参数,因此如果拷贝构造对传入的是右值,那么就会调用移动构造函数(最匹配原则)。
  • string 的拷贝构造函数做的是深拷贝,而移动构造函数中只需要调用 swap 函数进行资源的转移,因此移动构造函数的代价比拷贝构造的代价小。
3.5.2.2 移动赋值

**概念:**移动赋值是一个赋值运算符的重载,他跟拷贝赋值函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类型的引用,但不同的是要求这个参数是右值引用。

**本质:**移动赋值也是将传入右值的资源窃取过来,占为己有,这样就避免了深拷贝,所以它叫移动赋值,就是窃取别人的资源来赋值给自己的意思。

示例:

namespace tcq
{
	class string
	{
	public:
        // 构造函数
        string(const char* str = "")
 			:_size(strlen(str))
 			, _capacity(_size)
 		{
 			cout << "string(char* str)-构造" << endl;
 			_str = new char[_capacity + 1];
 			strcpy(_str, str);
 		}
        
 		void swap(string& s)
 		{
 			::swap(_str, s._str);
 			::swap(_size, s._size);
 			::swap(_capacity, s._capacity);
 		}
        
        // 拷贝构造
 		string(const string& s)
 			:_str(nullptr)
 		{
 			cout << "string(const string& s) -- 拷⻉构造" << endl;
            reserve(s._capacity);
 			for (auto ch : s)
 			{
 				push_back(ch);
 			}
 		}

		//移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}
        
        		//移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
        
        // 析构函数
        ~string()
 		{
 			cout << "~string() -- 析构" << endl;
 			delete[] _str;
 			_str = nullptr;
 		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

int main()
{
    tcq::string s1;
 	s1 = tcq::string("yyyyy");
 
 	return 0;
}   

分析:

这里和上面的分析一样,移动赋值和移动构造本质都是将一块将亡值的区间通过直接掠夺的方式用来初始化因为的变量,从而达到节省空间提升效率的目的。

同时需要注意,这里在返回局部的string对象时,会先调用移动构造生成一个临时对象,然后再调用移动赋值将临时对象的资源转移给接收返回值的对象 s1 ,这个过程虽然调用了两个函数,但这两个函数要做的只是资源的移动,而不需要进行深拷贝,大大提高了效率。

移动赋值和原有 operator= 函数的区别:

  • 在没有增加移动赋值之前,由于原有 operator= 函数采用的是 const 左值引用接收参数,因此无论传入的是左值还是右值,都会调用原有的 operator= 函数。

  • 增加移动赋值之后,由于移动赋值采用的是右值引用接收参数,因此如果传入的是右值,那么就会调用移动赋值函数(最匹配原则)。

  • string 类的原有 operator= 函数做的是深拷贝,而移动赋值函数中只需要调用 swap 函数进行资源的转移,因此移动赋值的代价比调用原有 operator= 的代价小。

3.5.3 右值引用和移动语义解决传值返回问题

在介绍了右值引用、移动构造和移动赋值的相关知识之后,就需要解决一开始左值引用无法解决的传值返回问题。

示例代码:

namespace tcq
{ 
 	string addStrings(string num1, string num2)
 	{
 		string str;
 		int end1 = num1.size() - 1, end2 = num2.size() - 1;
 		int next = 0;
 		while (end1 >= 0 || end2 >= 0)
 		{
 			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
 			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
            
 			int ret = val1 + val2 + next;
 			next = ret / 10;
 			ret = ret % 10;
            
 			str += ('0' + ret);
		}
        if (next == 1)
 			str += '1';
        
 		reverse(str.begin(), str.end());
        
 		cout << "******************************" << endl;
        
 		return str;
 	}
}

// 场景1 
int main()
{
 	tcq::string ret = bit::addStrings("11111", "2222");
 	cout << ret.c_str() << endl;
    
 	return 0;
}

// 场景2 
int main()
{
 	tcq::string ret;
 	ret = bit::addStrings("11111", "2222");
 	cout << ret.c_str() << endl;
    
 	return 0;
}
3.5.3.1 右值对象构造,只有持参构造,没有移动构造的场景
  • 图1展示了 vs2019 debug 环境下编译器对持参构造的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的持参合二为一变为一次拷贝构造。
  • 需要注意的是在 vs2019 的 release 和 vs2022的debug 和 release,下面代码优化为非常恐怖,会直接将 str 对象的构造,str 拷贝构造调用时对接,临时对象拷贝构造 ret 对象,会三合一,变为直接构造。要理解这个优化需要结合目标生命期和栈帧的角度理解,如图3所示。
  • linux下可以将下面代码拷贝到 test.cpp 文件,编译时加上 g++ test.cpp -fno-elide-constructors 的方式关掉构造优化,运行结果可以看到图1左边没有优化的两次拷贝构造。

图一:

在这里插入图片描述

3.5.3.2 右值对象构造,有拷贝构造,也有移动构造的场景
  • 图2展示了 vs2019 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两个移动构造,右边为编译器优化的场景下连续步骤中的持参合二为一变为一次移动构造。
  • 需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release ,下面代码优化为非常恐怖,会直接将 str 对象的构造,str 拷贝构造调用时对接,临时对象拷贝构造 ret 对象,会三合一,变为直接构造。要理解这个优化需要结合目标生命期和栈帧的角度理解,如图3所示。
  • linux下可以将下面代码拷贝到test.cpp文件,编译时加上 g++ test.cpp -fno-elide-constructors 的方式关掉构造优化,运行结果可以看到图1左边没有优化的两次移动构造。

图2:

在这里插入图片描述

图3:

在这里插入图片描述

3.5.3.3 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
  • 图4左边展示了 vs2019 debug 和 g++ test.cpp -fno-elide-constructors 环境下编译器的处理,一次拷贝构造,一次拷贝赋值。

  • 需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release ,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度度用对象的引用。运行结果的角度,可以看到 str 的构造是在赋值后,说明str就是临时对象的别名。

图4:

在这里插入图片描述

3.5.3.4 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
  • 图5 左边展示了 vs2019 debug 环境 和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。
  • 需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到 str 的构造是在赋值以后,说明 str 就是临时对象的别名。

图5:

在这里插入图片描述

3.5.3.5 总结

综上,只要使用了移动构造和移动赋值,就算编译器没有进行优化,程序运行仍然有很高的效率。程序效率就不再依赖于编译器。

C++11 设计出右值引用之后,为 STL 的所有容器都提供了移动构造和移动赋值,包括容器适配器:

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

3.5.4 右值引用和移动语义在传参中的提效

查看 STL 文档我们发现 C++11 以后容器的 pushinsert 系列的接口是否增加了右值引用版本

在这里插入图片描述

在这里插入图片描述

这些容器的接口在参数部分适配了右值引用,使得接口的功能执行的更加高效。

具体示例:

// void push_back (const value_type& val);
// void push_back (value_type&& val);
// iterator insert (const_iterator position, value_type&& val);
// iterator insert (const_iterator position, const value_type& val);

int main()
{
    // 示例1:左值尾插
 	std::list<bit::string> lt;
 	tcq::string s1("111111111111111111111");
 	lt.push_back(s1);
 	cout << "*************************" << endl;
    
    // 示例2:匿名对象(右值)尾插
 	lt.push_back(bit::string("22222222222222222222222222222"));
 	cout << "*************************" << endl;
    
    // 示例3:隐式类型转换生成的临时对象(右值)尾插
 	lt.push_back("3333333333333333333333333333");
 	cout << "*************************" << endl;
    
    // 示例4:move强转为右值尾插
 	lt.push_back(move(s1));
 	cout << "*************************" << endl;
    
 	return 0;
}

分析:

  • 示例1:

    // 运行结果
    string(char* str) -- 构造
    string(const string& s) -- 拷⻉构造
    *************************
    

    首先直接构造一个 string 类的 s1 左值对象,然后因为是左值所以通过拷贝构造将 s1 中的内容尾插到 list 类中去。

  • 示例2:

    // 运行结果
    string(char* str) -- 构造
    string(string&& s) -- 移动构造
    ~string() -- 析构
    *************************
    

    首先直接构造一个 string 类的匿名对象,然后直接将其尾插到 list 类的对象中,因为匿名对象是右值走移动构造,与待插入的 listnode 交换资源,并析构原来用于存储匿名对象的空间,提升效率节省空间。

  • 示例3:

    // 运行结果
    string(char* str) -- 构造
    string(string&& s) -- 移动构造
    ~string() -- 析构
    *************************
    

    这里的情况与示例2基本一样,但是这里首先因为 string 类支持单参数构造所以通过隐式类型转换将 "3333333333333333333333333333" 构造成了一个 string 类的临时对象,再将其尾插到 list 类的对象中,因为临时对象也是右值所以后续分许与示例2一致不做过多分析。

  • 示例4:

    // 运行结果
    string(string&& s) -- 移动构造
    *************************
    

    这里使用 move 函数将原本为左值的 s1 强转为右值,从而通过移动构造实现将 s1 尾插到 list 对象中的目的,这样避免了原本为左值插入时需要的拷贝构造,节省了空间提高了效率。但是需要注意,如果使用这个方法尾插后的 s1 就被析构清空了。

模拟实现右值插入和尾插

#pragma once
#include <iostream>
#include <assert.h>
#include <algorithm>

namespace tcq
{
    // list的节点
	template<class T>
	struct list_node 
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		// 普通构造
		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}

		// 移动构造
		list_node(T&& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(x))
		{}
	};

    // list的迭代器
	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		//...
	};

	// list 类
	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		void empty_initialize() 
        {  // 初始化 -- 哨兵位头结点
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;  // 空间换时间,用于标记节点个数
		}

		list() 
        {
			empty_initialize();
		}

        // 尾插左值版本
		void push_back(const T& x) 
        {
			insert(end(), x);  //复用
		}
		
        // 尾插右值版本
		void push_back(T&& x) 
        {
			insert(end(), move(x));  //复用
		}

        // 头插左值版本
		void push_front(const T& x) 
        {
			insert(begin(), x);  //复用
		}
        
        // 头插右值版本
        void push_front(T&& x) 
        {
			insert(begin(), move(x));  //复用
		}

        // 插入左值版本
		iterator insert(iterator pos, const T& x) 
        {
			node* newnode = new node(x);
			node* cur = pos._pnode;
			node* prev = cur->_prev;

			prev->_next = newnode;
			newnode->_prev = prev;
			cur->_prev = newnode;
			newnode->_next = cur;

			++_size;
			return iterator(pos);
		}

        // 插入右值版本
		iterator insert(iterator pos, T&& x) 
        {
			node* newnode = new node(move(x));
			node* cur = pos._pnode;
			node* prev = cur->_prev;

			prev->_next = newnode;
			newnode->_prev = prev;
			cur->_prev = newnode;
			newnode->_next = cur;

			++_size;
			return iterator(pos);
		}

	private:
		node* _head;
		size_t _size;
	};
}

注意:

  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
  • 当实参是一个右值时,容器内部则调用移动构造,将右值对象的资源移动到容器空间的对象上

在模拟实现 insert 和 push_back 时需要注意,不能仅仅修改参数,因为前文提到尽管参数 x 已经和右值绑定为右值引用,但是其本身仍是左值属性,所以会出现上层的 insert 调用底层 new 一个新结点的时候会走原本的构造函数而不是移动移动构造,从而申请出一个左值的新结点;上层的 push_back 调用底层的 insert 时因为 x 为左值属性所以会匹配上插入左值版本,而不是模拟实现的插入右值版本。就会出现于预期向背的情况,所以不仅需要更改函数的参数,还需要在函数体中使用 move 将左值属性的 x 强转为右值属性,并且还需要手动实现一个移动构造,用于构造右值结点

在这里插入图片描述

3.6 类型分类

  • C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
    • 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。
      • 如:42truenullptr或者类似 str.substr(1, 2)str1 + str2 传值返回函数调用,或者整形 aba++a+b 等。纯右值和将亡值是 C++11 中提出的,C++11 中的纯右值概念划分等价于 C++98 中的右值。
    • 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达式。
      • move(x)static_cast<T&&>(x)
    • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值

3.7 万能引用和完美转发

3.7.1 引用折叠
  • C++中不能直接定义引用的引用如 int&& && r = i;,这样写会直接报错,通过模板或 typedef 中的类型操作可以构成引用的引用。
  • 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时 C++11 给出了一个引用折叠的规则: 右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用
  • 下面的程序中很好的展示了模板和 typedef 时构成引用的引用的引用折叠规则。

示例代码:

// 示例1:
int main()
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;

	lref& r1 = n; // r1 的类型是 int& 
	lref&& r2 = n; // r2 的类型是 int& 
	rref& r3 = n; // r3 的类型是 int& 
	rref&& r4 = 1; // r4 的类型是 int&& 
}

**分析:**以上代码展示引用折叠的书写方式和引用折叠的基本规则。

// 示例2:
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引用
template<class T>
void f1(T& x)
{}

int main()
{
    // 没有折叠->实例化为void f1(int& x) 
	f1<int>(n);
	f1<int>(0); // 报错 

	// 折叠->实例化为void f1(int& x) 
	f1<int&>(n);
	f1<int&>(0); // 报错 

	// 折叠->实例化为void f1(int& x) 
	f1<int&&>(n);
	f1<int&&>(0); // 报错 

	// 折叠->实例化为void f1(const int& x) 
	f1<const int&>(n);
	f1<const int&>(0);

	// 折叠->实例化为void f1(const int& x) 
	f1<const int&&>(n);
	f1<const int&&>(0);
    
    return 0;
}

分析:

因为 f1 中的模版参数给的左值引用,所以不管传什么参数其都会折叠成左值引用,所以前面三个报错都是引用传递了右值参数产生了报错。

最后两个没有报错是因为实例化为 const int& xconst int& x 既可以接受左值为参数也可以接受右值为参数。

// 示例3:
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引用
template<class T>
void f2(T&& x)
{}

int main()
{
   // 没有折叠->实例化为void f2(int&& x) 
	f2<int>(n); // 报错 
	f2<int>(0);

	// 折叠->实例化为void f2(int& x) 
	f2<int&>(n);
	f2<int&>(0); // 报错 

	// 折叠->实例化为void f2(int&& x) 
	f2<int&&>(n); // 报错 
	f2<int&&>(0);
    
    return 0;
}

分析:

可以看到,不管实参为什么类型,模板函数都能正确接受并实例化为对应的引用类型,所以把形参为右值引用的函数模板叫做万能引用

3.7.2 万能引用

上面一个小结已经初次接触到了万能引用,但是在平时开发过程中并不会这样实例化函数模版,而是直接给一个对象让编译器自行推理。示例代码如下:

template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	x++;

	cout << &a << endl;
	cout << &x << endl << endl;
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t) 
	Function(10); // 右值 

	int a;
	// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t) 
	Function(a); // 左值 

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t) 
	Function(std::move(a)); // 右值 

	const int b = 8;
	// b是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
	// 所以Function内部会编译报错,x不能++ 
	Function(b); // const 左值 

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	// 所以Function内部会编译报错,x不能++ 
	Function(std::move(b)); // const 右值 

	return 0;
}

分析:

Function(T&& t) 函数模板程序中,假设实参是 int 右值,模板参数 T 的推导为 int;实参是 int 左值,;再结合引用折叠规则,就实现了实参是左值实例化出左值引用版本形参的 Function,实参是右值时实例化出右值引用版本形参的 Function

而 Function 函数内部的函数体的作用是:当模板参数 T 的推导为 int ,打印出的 x 和 a 的地址不同。当模板参数 T 的推导为 int& ,此时 x 是 a 的别名,此时打印出来的 x 和 a 的地址是相同的。

总结:

只要传的参数是右值,T 推导的时候都会推导成 int ,模版实例化后走右值引用。只要传的参数是左值,T 推导的时候都会推导成 int&,模版实例化后走左值引用 。这样就形成了万能引用

3.7.3 完美转发

问题引入:

上面讲解了万能引用,但是万能引用存在一个很大的问题:万能引用实例化后函数的形参的属性全部都是左值

  • 如果实参为左值/ const 左值,则实例化函数的形参是左值/ const 左值。
  • 如果实参是右值/ const 右值,虽然实例化函数的形参是右值引用/ const 右值引用,但是右值引用本身是左值。

所以会有以下这种情况:

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }


template<class T>
void Function(T&& t)
{
	Fun(t);
}

int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t) 
	Function(10);			// 右值 

	int a;
	// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t) 
	Function(a);			// 左值 

	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t) 
	Function(std::move(a));	// 右值 

	const int b = 8;
	// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
	Function(b);			// const 左值

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	Function(std::move(b)); // const 右值 

	return 0;
} 
// 运行结果:
左值引用
const 左值引用
左值引用
const 左值引用

可以看到不管实参是什么类型最后输出的都是左值。这是因为虽然万能引用可以同时兼容左值引用和右值引用,但是不管是左值引用还是右值引用,形参 t 都是左值属性,所以在 Function 函数体中复用的函数 Fun 的形参所接收的一直都是左值。

同时,这里我们也不能简单的将 t move 后传递给 Fun 函数,因为这样会让 t 全部变为右值,又满足不了实参为左值的情况。

forward:

为了在传参的过程中能够保留对象原生类型属性,C++11 又设计出了完美转发 – forward。如下:

在这里插入图片描述

利用 forward 重新改写上述程序,即可解决问题:

template<class T>
void Function(T&& t)
{
	Fun(forward<T>(t));
}
// 运行结果:
左值引用
const 左值引用
右值引用
const 右值引用 

4. 可变参数模版

4.1 可变参数模版的概念

可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让程序员创建可以接受可变参数的函数模板和类模板。

  • C++11 之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。
  • C++11 之前其实也有可变参数的概念,比如 printf 函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。

4.2 可变参数基本语法及原理

4.2.1 基本语法和使用方法

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... 指出接下来的参数表示零或多个类型列表。
    • 函数参数列表中,类型名后面跟 ... 指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。
  • 模板参数 Args 前面有省略号,代表它是一个可变模板参数,把带省略号的参数称为参数包,参数包里面可以包含0到 N ( N ≥ 0 ) N ( N ≥ 0 ) N(N0) 个模板参数,而 args 则是一个函数形参参数包。
  • 模板参数包 Args 和函数形参参数包 args 的名字可以任意指定,并不是说必须叫做 Args 和 args 。

示例代码:

template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;// 获取参数包中参数的个数
}

int main()
{
	double x = 2.2;

	Print(); 						// 包里有0个参数,输出 0
	Print(1); 						// 包里有1个参数,输出 1
	Print(1, string("xxxxx")); 		// 包里有2个参数,输出 2
	Print(1.1, string("xxxxx"), x); // 包里有3个参数,输出 3

	return 0;
}

将 Print 函数定义为可变参数函数模版,后面调用的时候就可以传入任意数量的参数了,并且这些参数可以是不同类型的。

并且这里可以使用 sizeof... 运算符去计算参数包中参数的个数。

4.2.2 基本原理

原理1:编译时的实例化和引用折叠

本质:可变参数模板在编译时会根据实际调用参数的数量和类型,生成(实例化)具体的函数版本。

引用折叠规则:由于使用了万能引用 Args&&... args,编译器会结合引用折叠规则处理参数:

  • 如果参数是左值(lvalue),则折叠为左值引用(&)。
  • 如果参数是右值(rvalue),则折叠为右值引用(&&)。

实例化示例:

以上面 4.2.1 中的具体代码为示例:

// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数 
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

对于 Print();:实例化为 void Print();(无参数)。

对于 Print(1);:实例化为 void Print(int&& arg1);(1 是右值,整型)。

对于 Print(1, string("xxxxx"));:实例化为 void Print(int&& arg1, string&& arg2);(两个右值)。

对于 Print(1.1, string("xxxxx"), x);:实例化为 void Print(double&& arg1, string&& arg2, double& arg3);(前两个右值,x 是左值,双精度浮点)。

原理2:更本质的机制——类型泛化和数量变化

没有可变参数模板时的实现:在 C++11 之前,要实现类似功能,需要手动编写多个重载的函数模板,每个模板对应固定数量的参数。这很繁琐,且无法处理任意数量。如下代码示例:

// 无参数版本
void Print();

// 1个参数版本
template <class T1>
void Print(T1&& arg1);

// 2个参数版本
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);

// 3个参数版本
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);

// ... 以此类推,需要更多参数时继续添加

局限性:这种方式只能支持有限数量的参数(例如最多3个),如果需要更多,就必须添加更多模板。代码冗余且不灵活。

**核心原理:**当 C++11 补充了可变参数模版的设定后,当定义一个可变参数模板,编译器不会预先生成“很多个”固定参数的模板版本。相反,它在遇到具体调用时(如 Print(1, "str", 3.14)),才会根据调用参数的数量和类型,动态生成(实例化)一个具体的函数版本。这个生成过程是隐式的、自动的,由编译器根据模板参数包(...Args)和函数参数包(...args)来展开。

4.3 包展开

上面可以知道可以通过 sizeof... 计算参数包中参数的个数。但是无法直接获取参数包中的每个参数,只能通过展开参数包的方式来获取,这是使用可变参数模板的一个主要特点,也是最大的难点。要获取参数包中的各个参数,只能通过展开参数包的方式来获取,一般会通过递归逗号表达式来展开参数包。

递归:

当扩展一个包时,还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。然后 通过在模式的右边放一个省略号(...)来触发扩展操作。底层的实现细节如下图所示。

在这里插入图片描述

逗号表达式:

C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。

// 处理参数包中的每个参数
template <class T>
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}

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


// 本质可以理解为编译器编译时,包的扩展模式 
// 将上⾯的函数模板扩展实例化为下⾯的函数 
//void Print(int x, string y, double z)
//{
// 	Arguments(GetArg(x), GetArg(y), GetArg(z));
//}

4.3 empalce系列接口

4.3.1 empalce的介绍

C++11标准给STL中的容器增加 emplace 版本的插入接口,比如 list 容器的 push_front 、 push_back 和 insert 函数,都增加了对应的 emplace_front 、emplace_back 和 emplace 函数。如下:

在这里插入图片描述

这些 emplace 版本的插入接口支持模板的可变参数,比如 list 容器的 emplace_back 函数的声明如下:

在这里插入图片描述

注意: emplace系列接口的可变模板参数类型都带有“&&”,这个表示的是万能引用,而不是右值引用。

4.3.2 empalce的使用方法

emplace 系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。

list 容器的 emplace_backpush_back 为例:

  • 调用 push_back 函数插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
  • 调用 emplace_back 函数插入元素时,也可以传入左值对象或者右值对象,但不可以使用列表进行初始化。
  • 除此之外,emplace 系列接口最大的特点就是,插入元素时可以传入用于构造元素的参数包。

示例1:

int main()
{
 	list<bit::string> lt;
    
 	// 传左值,跟push_back⼀样,⾛拷⻉构造 
 	tcq::string s1("111111111111");
 	lt.emplace_back(s1);			// 输出:构造 + 拷贝构造
    lt.push_back(s1);				// 输出:构造 + 拷贝构造
 	cout << "*********************************" << endl;
    
 	// 右值,跟push_back⼀样,⾛移动构造 
 	lt.emplace_back(move(s1));		// 输出:构造 + 移动构造
    lt.push_back(move(s1));			// 输出:构造 + 移动构造
 	cout << "*********************************" << endl;
}

分析:

在首先构造出 string 类对象 s1 之后,不管是单纯的使用 emplace_back 插入还是 push_back 插入输出结果都是一样的。以左值形式传入就是走拷贝构造到 list 类对象 It 的新结点中去,以右值形式传入就是走移动构造到 list 类对象 It 的新结点中去。

**注意:**这里的第一个构造是 string 类对象 s1 的构造。

示例2:

int main()
{
    // 直接把构造string参数包往下传,直接⽤string参数包构造string 
 	// 这⾥达到的效果是push_back做不到的 
 	lt.emplace_back("111111111111");	// 输出:构造
    lt.push_back("111111111111");		// 输出:构造 + 移动构造 + 析构
  	cout << "*********************************" << endl;  
}

分析:

这里二者的使用开始出现差异。

首先解释 push_back 函数的运行结果,push_back 函数的声明如下 void push_back(value_typed&& val) ,这里的 value_typed 在 It 实例化的时候就已经确定为 string ,所以这里直接传入参数 "111111111111" ,其首先会因为 string 类支持单参数隐式类型转换,所以会构造出一个 string 类的临时对象存放 "111111111111" ,之后又因为临时对象是右值则通过移动构造与 It 中的节点交换资源,并释放析构掉原来的那块空间。

其次再解释 emplace_back 函数的运行结果,这里的 emplace_back 函数因为可变参数,这里直接把构造 string 参数包往下传,直接⽤ string 参数包构造 It 对象中的待插入节点,提升了效率。

示例3:

int main()
{
    list<pair<bit::string, int>> lt1;
 	// 跟push_back⼀样 
 	// 构造pair + 拷⻉/移动构造pair到list的节点中data上 
 	pair<bit::string, int> kv("苹果", 1);
 	lt1.emplace_back(kv);			// 输出:构造pair + 拷贝构造
    lt1.push_back(kv);				// 输出:构造pair + 拷贝构造
 	cout << "*********************************" << endl;
    
 	// 跟push_back⼀样 
 	lt1.emplace_back(move(kv));		// 输出:构造pair + 移动构造
    lt1.push_back(move(kv));		// 输出:构造pair + 移动构造
 	cout << "*********************************" << endl;
}

分析:

这里和示例1的结果的情况一样,不会因为单参数和多参数而发生结果的改变。

示例4:

int main()
{
    // 直接把构造pair参数包往下传,直接⽤pair参数包构造pair 
 	// 这⾥达到的效果是push_back做不到的 
 	lt1.emplace_back("苹果", 1);		// 输出:构造
    lt1.push_back({"苹果", 1});		// 输出:构造 + 移动构造 + 析构
 	cout << "*********************************" << endl
}

分析:

这里有发生了结果的差异,来分别分析一下原因。

首先解释 emplace_back 函数的运行结果,这里的 emplace_back 函数的可变参数接收到实参之后,经过推演得到了一个是 const char* 类型,一个是 int 类型的参数包,然后直接把参数包往下传,直接用这里的参数包构造 It 对象中的待插入节点。

其次解释 push_back 函数的运行结果,首先需要注意这里push_back 函数的参数类型已经被模版实例化所确定为 pair ,所以传参数进行隐式类型转换时需要按照多参数的传递方式使用花括号括起来传递。同时因为经历了隐式类型转换从而生成了一个临时对象用于存储 {"苹果", 1} ,之后再通过移动构造尾插到 It 对象的待插入节点中。最后还需要将原空间析构。

总结:

emplace_back 总体⽽⾔是更⾼效,推荐以后使用 emplace 系列替代 insert 和 push 系列。

4.3.3 empalce系列的模拟实现

下面模拟实现了 listemplaceemplace_back 接口,这里把参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型 T 的构造,所以达到了前面说的 emplace 支持直接插入构造 T 对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造 T 对象。

完整代码:

// List.h
namespace tcq
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(T&& data)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))
		{}
		template <class... Args>
		ListNode(Args&&... args)
			: _next(nullptr)
			, _prev(nullptr)
			, _data(std::forward<Args>(args)...)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}

		// ++it;
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		Ref operator*()
		{
			return _node->_data;
		}

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_back(T&& x)
		{
			insert(end(), move(x));
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			// prev newnode cur

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}

		iterator insert(iterator pos, T&& x)
		{
			Node * cur = pos._node;
			Node* newnode = new Node(move(x));
			Node* prev = cur->_prev;

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

		template <class... Args>
		void emplace_back(Args&&... args)
		{
			insert(end(), std::forward<Args>(args)...);
		}

		// 原理:本质编译器根据可变参数模板⽣成对应参数的函数 
		/*void emplace_back(string& s)
		{
			insert(end(), std::forward<string>(s));
		}

		void emplace_back(string&& s)
		{
			insert(end(), std::forward<string>(s));
		}

		void emplace_back(const char* s)
		{
			insert(end(), std::forward<const char*>(s));
		}
		*/

		template <class... Args>
		iterator insert(iterator pos, Args&&... args)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(std::forward<Args>(args)...);
			Node* prev = cur->_prev;

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}
	private:
		Node* _head;
	};
}
// List.h

namespace bit
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(T&& data)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))
		{}
		template <class... Args>
		ListNode(Args&&... args)
			: _next(nullptr)
			, _prev(nullptr)
			, _data(std::forward<Args>(args)...)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}

		// ++it;
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		Ref operator*()
		{
			return _node->_data;
		}

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};

	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_back(T&& x)
		{
			insert(end(), move(x));
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			// prev newnode cur

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}

		iterator insert(iterator pos, T&& x)
		{
			Node * cur = pos._node;
			Node* newnode = new Node(move(x));
			Node* prev = cur->_prev;

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}

		template <class... Args>
		void emplace_back(Args&&... args)
		{
			insert(end(), std::forward<Args>(args)...);
		}

		// 原理:本质编译器根据可变参数模板⽣成对应参数的函数 
		/*void emplace_back(string& s)
		{
			insert(end(), std::forward<string>(s));
		}

		void emplace_back(string&& s)
		{
			insert(end(), std::forward<string>(s));
		}

		void emplace_back(const char* s)
		{
			insert(end(), std::forward<const char*>(s));
		}
		*/

		template <class... Args>
		iterator insert(iterator pos, Args&&... args)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(std::forward<Args>(args)...);
			Node* prev = cur->_prev;

			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;

			return iterator(newnode);
		}
	private:
		Node* _head;
	};
}

5. 类的新功能

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

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

默认的移动构造:

  • 如果没有手动实现移动构造函数,且没有实现析构函数、拷贝构造函数、拷贝赋值运算符重载中的任意一个,那么编译器会自动生成一个默认移动构造函数。默认生成的移动构造函数对于内置类型成员会执行逐成员按字节拷贝**(浅拷贝)**,对于自定义类型成员,则会检查该成员是否实现了移动构造函数:如果实现了就调用移动构造,否则调用拷贝构造。

默认的移动赋值

  • 如果没有手动实现移动赋值运算符重载函数,且没有实现析构函数、拷贝构造函数、拷贝赋值运算符重载中的任意一个,那么编译器会自动生成一个默认移动赋值运算符。默认生成的移动赋值函数对于内置类型成员会执行逐成员按字节拷贝**(浅拷贝)**,对于自定义类型成员,则会检查该成员是否实现了移动赋值:如果实现了就调用移动赋值,否则调用拷贝赋值。(默认移动赋值与移动构造函数的行为完全类似)

如果提供了移动构造函数或者移动赋值运算符,编译器不会自动提供拷贝构造函数和拷贝赋值运算符。

示例代码1:

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

int main()
{
	Person s1;					// 输出:构造
	Person s2 = s1;				// 输出:拷贝构造
	Person s3 = std::move(s1);	// 输出:移动构造
   
    Person s4;
	s4 = std::move(s2);			// 输出:移动赋值

	return 0;
}

分析:

这里没有手动的实现 Person 类的移动构造和移动赋值函数所以会自动生成默认的移动构造和移动赋值,对于类中的内置类型直接进行浅拷贝构造,自定义类型则是调用 string 类中已经存在的移动构造和移动赋值。

示例代码2:

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

int main()
{
	Person s1;					// 输出:构造
	Person s2 = s1;				// 输出:拷贝构造
	Person s3 = std::move(s1);	// 输出:拷贝构造
   
    Person s4;
	s4 = std::move(s2);			// 输出:拷贝赋值

	return 0;
}

分析:

因为这里显示写了一个析构函数,所以就无法生成默认的移动构造和移动赋值,所以这里无论是左值还是右值都只能走拷贝构造和拷贝赋值。

总结:

这里的移动构造和移动赋值和之前类和对象中介绍的析构函数、拷贝构造和赋值重载一样,只有在类成员变量涉及深拷贝时才需要进行手动实现,不然仅仅依靠默认生成的即可。

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

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

这是类内初始化的核心作用,作为初始化列表的“后备”机制。它不会取代初始化列表,而是补充它。优先级是:构造函数初始化列表 > 类内初始化 > 默认构造(如果都没有)

这种做法的主要作用:

  • 提供默认初始化后备:如果构造函数的初始化列表中没有显式初始化该成员,它会自动使用类内默认值。这确保成员总有值,避免未初始化bug。

  • 简化多构造函数场景:在一个有多个构造函数的类中,不必在每个初始化列表中重复写默认值。默认值集中在声明处,便于修改和阅读。

    eg:假设一个类有3个构造函数,以前需要在每个构造函数的初始化列表中写: _age(1);现在只需在声明处写int _age = 1;,其他构造函数如果不覆盖,就自动用1。

  • 提高代码可读性和维护性:默认值与成员声明在一起,一目了然。修改默认值只需改一处,不用搜索所有构造函数。

// 示例类:展示类内初始化的作用
class Person
{
public:
    // 默认构造函数:无参数,_age 使用类内默认值1(未在初始化列表指定)
    Person()
        : _name("")  // 只初始化_name,_age 自动用类内值
    {}

    // 带name的构造函数:_age 使用类内默认值1(未指定)
    Person(const char* name)
        : _name(name)
    {}

    // 原模板构造函数:带age参数,这里显式初始化_age(覆盖类内默认值)
    Person(const char* name = "", int age = 1)  // 添加age默认值1,使其可选
        : _name(name)
        , _age(age)  // 显式使用age,优先于类内默认
    {}

    ~Person()
    {}

private:
    tcq::string _name;
    int _age = 1;  // 类内初始化:作为后备默认值,在未指定时生效
};

分析:

Person()Person(const char* name) 中,没有在初始化列表指定_age,所以它自动使用int _age = 1; 的默认值。这展示了如何在多构造函数中统一默认值,而不必重复代码。

在带age参数的构造函数中,初始化列表覆盖了默认值,体现了优先级。

5.3 defult和delete

  • C++11 可以让程序员更好地控制要使用的默认函数。假设要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:程序员提供了拷贝构造,就不会生成移动构造了,那么可以使用 =default 关键字显示指定移动构造生成。

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

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(Person&& p) = default;

	//Person(const Person& p) = delete;

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

5.4 final和override

  • final的作用:final可以修饰一个类这个类/虚函数不能被继承。防止类被继承或虚函数被重载,控制继承深度。
  • **override的作用:**override可以放到派生类的虚函数中检查这个虚函数是否完成重写。确保虚函数正确重载,提供编译时安全检查。
// 添加基类 BasePerson,包含虚函数
class BasePerson 
{
public:
    BasePerson() {}

    // 虚函数:可被重载
    virtual void printInfo() const 
    {
        std::cout << "BasePerson info" << std::endl;
    }

    // final虚函数:不能被任何派生类重载
    virtual int getAge() const final 
    {
        return _age;
    }

protected:
    int _age = 0;  // 保护成员,供派生类访问
};

// Person类:继承自BasePerson,并标记为final(防止进一步继承)
class Person final : public BasePerson 
{
public:

    Person(const char* name = "", int age = 0)
        : BasePerson()  // 调用基类构造函数
        , _name(name)
        , _age(age)  // 注意:_age现在是protected继承的,但这里我们有private版本
    {}

    // 拷贝构造函数
    Person(const Person& p)
        : BasePerson(p)  // 调用基类拷贝
        , _name(p._name)
        , _age(p._age)
    {}
    
	// 移动构造函数,使用default
    Person(Person&& p) = default;

    // 使用override:重载基类的printInfo()
    virtual void printInfo() const override 
    {
        std::cout << "Person: " << _name << ", Age: " << _age << std::endl;
    }

    // 尝试重载final函数:如果取消注释,会编译错误(因为基类标记了final)
    // virtual int getAge() const override { return _age + 1; }  // 错误:不能重载final函数

private:
    tcq::string _name;
    int _age;  // 注意:这会隐藏基类的_age,如果需要访问基类,用BasePerson::_age
};

分析:

防止类被继承class Person final 使Person不能被任何类继承(注释掉的Child类演示了这一点,如果取消注释,会导致编译错误)。这控制了继承深度,适合当想“密封”一个类,不允许进一步扩展时。

防止虚函数被重载BasePerson::getAge() const final 禁止任何派生类(如Person)重载它(注释掉的尝试重载演示了错误)。这确保关键函数的行为固定不变。

6. STL中的一点变化

  • 下图 1 圈起来的就是 STL 中的新容器,但是实际最有用的是 unordered_mapunordered_set。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。

  • STL 中容器的新接口也不少,最重要的就是右值引用和移动语义相关的 push/insert/emplace 系列接口和移动构造和移动赋值,还有 initializer_list 版本的构造等,这些前面都讲过了,还有一些无关痛痒的如 cbegin/cend 等需要时查查文档即可。

  • 容器的范围 for 遍历,这个在容器部分也介绍过了。

在这里插入图片描述

7. lambda

7.1 问题引入

在 C++98 中,为了替代函数指针,C++ 设计出了仿函数,也称为函数对象,仿函数实际上就是一个普通的类,只是该类重载了函数调用操作符 (),这使得该类的对象可以像函数一样去使用,如下:

template<class T>
struct Compare
{
    bool operator()(int a, int b)
    {
        return a < b;
    }
};

int main()
{
    Compare<int> com;
    int a = 1;
    int b = 2;
    cout << com(a, b) << endl;
}

// 运行结果:
// 1

虽然仿函数已经能够完全取代函数指针了,但是它在如下场景下仍然有些难用:

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

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个 algorithm 算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了 lambda 表达式。

7.2 lambda表达式语法

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

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

  • lambda 表达式的格式:[capture-list] (parameters) -> return type { function body }

    • [capture-list]捕获列表,该列表总是出现在 lambda 函数的开头位置,编译器根据 [] 来判断接下来的代码是否为 lambda 函数,捕获列表能够捕捉上文中的变量供 lambda 函数使用,捕获列表可以传值和传引用,具体细节在下文中细讲。捕获列表为空也不能省略。
    • (parameters)参数列表,与普通函数的参数列表类型类似,如果不需要参数传递,则可以省略 () 里内容。
    • ->return type返回值类型,用箭头返回类型形式声明函数的返回类型,没有返回值时此部分可以省略。一般返回类型明确时,也可以省略,编译器根据返回类型进行推导。
    • {function body}函数体,函数体的实质跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

注意:在 lambda 函数定义中,参数列表和返回值类型都是可选部分,即可以省略不写,同时捕捉列表和函数体也可以为空,因此 C++11 中最简单的 lambda 函数为:[]{}; 但该 lambda 函数无意义。

同时在上面那个场景中使用 lambda 表达式的方式如下:

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
   }, { "菠萝", 1.5, 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;
}

7.3 lambda 表达式与函数对象

lambda 表达式和仿函数一样,本质上也是一个可调用的函数对象,所以 lambda 表达式的使用方式和仿函数完全相同。但和仿函数不同的是,lambda 表达式的类型是由编译器自动生成的,并且带有随机值,所以无法具体写出 lambda 表达式的类型,只能使用 auto 进行推导。

struct Compare 
{
	bool operator()(int a, int b) 
    {
		return a < b;
	}
};

int main() 
{
	int a = 1;
	int b = 2;

	//仿函数
	Compare com;
	cout << com(a, b) << endl;

	//lambda表达式
	auto func = [](int a, int b) ->bool { return a < b; };
	cout << func(a, b) << endl;

	return 0;
}

实际上,lambda 表达式的底层实现是通过编译器生成一个匿名的函数对象,然后再通过这个函数对象来调用 operator()() 函数,从而完成调用。换句话说,lambda 表达式底层实际上是通过替换为仿函数来完成的

7.4 lambda 表达式的捕捉列表

lambda 表达式最厉害的地方在于捕捉列表,捕捉列表可以捕捉父作用域中 lambda 表达式之前的所有变量,捕捉方式有如下几种:

  1. [var]:表示值传递方式捕捉变量var,传值捕捉到的参数默认是被 const 修饰的,所以不能在 lambda 表达式的函数体中修改它们。如果要修改,需要使用 mutable 修饰;但由于传值捕捉修改的是形参,所以一般也不会去修改它。

    #include <iostream>
    using namespace std;
        
    int main() 
    {
        int a = 1;
    
       	// 传值捕获a变量
        auto func = [a](int y) {
            return a + y;
        };
        cout << func(2) << endl; // 控制台输出:3
    
    	// 使用mutable修饰,使得形参a可以被改变,但是不会影响实参a
        auto func1 = [a](int y) mutable{
            a += 2;
            return a + y;
        }
        cout << func1(2) << endl;// 控制台输出:3
        cout << a << endl;// 控制台输出:1
    
        return 0;
    }
    
  2. [&var]:表示引用传递捕捉变量var,通过引用传递捕捉,就可以在 lambda 表达式函数体中修改实参的值了。

    #include <iostream>
    using namespace std;
        
    int main() 
    {
        int a = 0;
    
       	// 传引用捕获a变量
        auto func = [&a](int y) {
            a += 2;
            return a + y;
        };
        cout << func(2) << endl; // 控制台输出:4
        cout << a << endl;		 // 控制台输出:2
    
        return 0;
    }
    
  3. [&]:表示引用传递捕捉所有父作用域中的变量 (包括this)

    #include <iostream>
    using namespace std;
        
    int main() 
    {
        int a = 0;
        int b = 0;
    
       	// 传引用捕获所有变量
        auto func = [&](int y) {
            a = 3;
            b = 3;
            return a + y;
        };
        cout << func(2) << endl; 		// 控制台输出:5
        cout << a << ' ' << b << endl;	// 控制台输出:3 3
    
        return 0;
    }
    
  4. [=]:表示值传递方式捕获所有父作用域中的变量 (包括this)

    #include <iostream>
    using namespace std;
        
    int main() 
    {
        int a = 1;
        int b = 2;
    
       	// 传值捕获所有变量
        auto func = [&](int y) {
            return a + b + y;
        };
        cout << func(2) << endl; 		// 控制台输出:5
        cout << a << ' ' << b << endl;	// 控制台输出:1 2
    
        return 0;
    }
    
  5. 除了上面这四种捕捉方式之外,lambda 表达式的捕捉列表还支持混合捕捉,如下:

    #include <iostream>
    using namespace std;
        
    int main() 
    {
        int a = 1;
        int b = 2;
    
       	// 混合捕获,a变量用引用捕获,其余变量用传值捕获
        auto func = [=, &a](int y) mutable{
            a = 3;
            b = 3;
            return a + y;
        };
        cout << func(2) << endl; 		// 控制台输出:5
        cout << a << ' ' << b << endl;	// 控制台输出:3 2
    
        return 0;
    }
    

lambda 表达式有如下注意事项:

  1. 作用域是指包含 lambda 函数的语句块,捕捉列表可以捕捉父作用域中位于 lambda 函数之前定义的所有变量。
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割,比如:
    • [=, &a, &b]:以引用传递的方式捕捉变量 ab,值传递方式捕捉其他所有变量。
    • [&, a, this]:值传递方式捕捉变量 athis,引用传递方式捕捉其他变量。
  3. 捕捉列表不允许变量重复捕捉,否则会导致编译错误。
  4. 在块作用域中的 lambda 函数只能捕捉父作用域中的局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  5. lambda 表达式之间不能相互赋值,即便看起来类型相同。

8. 包装器

8.1 function

template <class T>
class function; // undefined

template <class Ret, class... Args>
class function<Ret(Args...)>;
  • std::function 是一个类模板,也是一个包装器。std::function 的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambdabind 表达式等,存储的可调用对象被称为 std::function 的目标。若 std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。

  • 以上是 function 的原型,它被定义在 <functional> 头文件中。std::function - cppreference.comfunction 的官方文件链接。

  • 函数指针、仿函数、lambda 等可调用对象的类型各不相同,std::function 的优势就是统一类型,对它们都可以进行包装,这样在很多地方就方便声明可调用对象的类型,下面的第二个代码样例展示了 std::function 作为 map 的参数,实现字符串和可调用对象的映射表功能。

#include<functional>

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

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

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()
{
	// 包装各种可调用对象 
	function<int(int, int)> f1 = f;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };

	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;

	// 包装静态成员函数 
	// 成员函数要指定类域并且前⾯加&才能获取地址 
	function<int(int, int)> f4 = &Plus::plusi;
	cout << f4(1, 1) << endl;

	// 包装普通成员函数 
	// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以 
	function<double(Plus*, double, double)> f5 = &Plus::plusd;
	Plus pd;
	cout << f5(&pd, 1.1, 1.1) << endl;

	function<double(Plus, double, double)> f6 = &Plus::plusd;
	cout << f6(pd, 1.1, 1.1) << endl;
	cout << f6(pd, 1.1, 1.1) << endl;

	function<double(Plus&&, double, double)> f7 = &Plus::plusd;
	cout << f7(move(pd), 1.1, 1.1) << endl;
	cout << f7(Plus(), 1.1, 1.1) << endl;

	return 0;
}

8.3 bind

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

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

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

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

#include<functional>

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

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

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

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

int main()
{
	auto sub1 = bind(Sub, _1, _2);
	cout << sub1(10, 5) << endl;

	// bind 本质返回的⼀个仿函数对象 
	// 调整参数顺序(不常⽤) 
	// _1代表第⼀个实参 
	// _2代表第⼆个实参 
	// ...
	auto sub2 = bind(Sub, _2, _1);
	cout << sub2(10, 5) << endl;

	// 调整参数个数 (常⽤) 
	auto sub3 = bind(Sub, 100, _1);
	cout << sub3(5) << endl;

	auto sub4 = bind(Sub, _1, 100);
	cout << sub4(5) << endl;

	// 分别绑死第123个参数 
	auto sub5 = bind(SubX, 100, _1, _2);
	cout << sub5(5, 1) << endl;

	auto sub6 = bind(SubX, _1, 100, _2);
	cout << sub6(5, 1) << endl;

	auto sub7 = bind(SubX, _1, _2, 100);
	cout << sub7(5, 1) << endl;

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

	// bind⼀般用于,绑死⼀些固定参数 
	function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
	cout << f7(1.1, 1.1) << endl;

	// 计算复利的lambda 
	auto func1 = [](double rate, double money, int year)->double 
		{
		double ret = money;
		for (int i = 0; i < year; i++)
		{
			ret += ret * rate;
		}
		return ret - money;
	};

	// 绑死⼀些参数,实现出⽀持不同年华利率,不同⾦额和不同年份计算出复利的结算利息 
	function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
	function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
	function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
	function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);

	cout << func3_1_5(1000000) << endl;
	cout << func5_1_5(1000000) << endl;
	cout << func10_2_5(1000000) << endl;
	cout << func20_3_5(1000000) << endl;

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值