【C++】C++11

国际标准化组织(ISO)和国际电工委员会(IEC)旗下的C++标准委员会(ISO/IEC JTC1/SC22/WG21)成立于1998年,致力于制定C++的标准。

在发布C++98标准后,委员会计划每五年视实际的需要来更新一次标准。五年后,委员会发布了C++03标准。不过,C++03主要是对C++98中的漏洞进行修复,语言的核心部分没有太大变动,因此人们习惯性地把这两个标准合并称为C++98/03标准。

在这之后,委员会又计划在2007年发布下一个标准,并定名为C++07。但这一次,计划没赶上变化,到2006年的时候,委员会认为2007年无法完成C++07,且2008年也可能完不成,于是,这个还未出世、一再拖更的标准被更名为了C++0X(x意味着,不清楚究竟在07/08/09哪一年完成)。然而直到2010年,C++0X也没完成。

总算在2011年,这个迟迟拖更的标准终于被发布了,并真正定名为C++11。

相比于C++98/03,C++11带来了可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。且C++11能更好地用于系统开发和库开发、语法更加泛化、简单化,也更加稳定和安全,不仅功能更强大,还能提升程序员的开发效率,在实际项目的开发中应用得越来越多。

C++11增加的语法特性非常之多,难以一一列举。在此,本篇博客整理了C++11中较为实用的语法,以供读者查阅学习。

目录

一、统一的列表初始化

[1.{ }列表初始化](#1.{ }列表初始化)

2.原理之std::initializer_list

二、声明

[1.关键字 auto](#1.关键字 auto)

[2.关键字 decltype](#2.关键字 decltype)

[3.关键字 nullptr](#3.关键字 nullptr)

三、范围for循环

四、STL中的一些变化

1.新容器(了解)

2.新接口(了解)

五、右值引用和移动语义

1.表达式的值类别

2.左值引用和右值引用

3.左值引用的缺陷

4.值类别的再探讨

5.移动(拷贝)构造和移动赋值

6.右值引用的价值

7.右值引用的属性和原理

8.完美转发与std::forward()

六、新的类功能

1.新的默认成员函数

[2.强制生成默认成员函数 - 关键字default](#2.强制生成默认成员函数 - 关键字default)

[3.禁止生成默认成员函数 - 关键字delete](#3.禁止生成默认成员函数 - 关键字delete)

4.缺省值初始化类成员变量

[5.重写相关 - 关键字final和override](#5.重写相关 - 关键字final和override)

七、可变参数模板

1.展开参数包

[· 递归函数方式](#· 递归函数方式)

[· 逗号表达式方式](#· 逗号表达式方式)

2.emplace系列接口

八、lambda表达式

1.仿函数的劣势

2.lambda表达式的运用

3.lambda表达式的大小

4.lambda表达式的原理

九、包装器

1.引例

[2.function<( )> - 封装函数](#2.function<( )> - 封装函数)

[3.bind() - 调整参数顺序](#3.bind() - 调整参数顺序)

十、智能指针

十一、线程库


一、统一的列表初始化

1.{ }列表初始化

C++98,支持使用"{ }"对数组或结构体的元素进行统一的列表初始值设定。

cpp 复制代码
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;
}

C++11,扩大了用"{ }"的使用范围,使"{ }"可用于所有的内置类型自定义类型 。使用"{ }"初始化,既可添加赋值"=",也可将其省去

cpp 复制代码
//内置类型

struct Point
{
   int _x;
   int _y;
};

int main()
{
    //普通的整型变量
    int x1 = 1;
    int x2{ 2 };                   //等价于"int x2 = 2;"

    //数组
    int array1[]{ 1, 2, 3, 4, 5 }; //array1中:1 2 3 4 5
    int array2[5]{ 0 };            //array2中:0 0 0 0 0

    //结构体
    //本质都是调用构造函数     
	Point p0(0, 0);
	Point p1 = { 1,1 };   //这是多参数的构造函数隐式类型转换
	Point p2{ 2,2 };      //p2中:2(_x)、2(_y)

    //new表达式 
	int* ptr1 = new int[3]{ 1,2,3 };     //ptr1中:1 2 3
	Point* ptr2 = new Point[2]{p0,p1};         //有名对象初始化
	Point* ptr3 = new Point[2]{ {0,0},{1,1} }; //匿名对象初始化

  return 0;
}
cpp 复制代码
//自定义类型

class Date
{
public:
     Date(int year, int month, int day)
         :_year(year)
         ,_month(month)
         ,_day(day)
     {
         cout << "Date(int year, int month, int day)" << endl;
     }
private:
     int _year;
     int _month;
     int _day;
};

int main()
{
     //c++98写法
     Date d1(2022, 1, 1); 

     //C++11写法
     //这里同样会调用构造函数来初始化
     Date d2{ 2022, 1, 2 };
     Date d3 = { 2022, 1, 3 };

     return 0;
}

【Tips】一切类型皆可用"{ }"初始化,且可以不写赋值"="(但建议定义变量一般不要去掉赋值"=")。

2.原理之std::initializer_list

C++11支持将"{ }"中的内容识别为一个内容不可修改的常量数组,通过initializer_list类的构造函数,构造一个initializer_list的对象,以此支持"{ }"的列表初始化。

"{ }"之所以能够完成列表初始化,本质上是因为在构造函数中完成了对initializer_list类对象的拷贝(理应是先调用构造函数,再调用拷贝构造,实际上编译器把这个过程优化成了直接调用构造函数)。

cpp 复制代码
struct Point
{
   int _x;
   int _y;
};

int main()
{
	auto il = { 10, 20, 30 };
	//等价于:initializer_list<int> il = { 10, 20, 30 };
    cout << typeid(il).name() << endl; //class std::initializer_list<int>
	cout << sizeof(il) << endl;        //16

	initializer_list<int>::iterator it1 = il.begin(); //支持迭代器,begin()返回列表第一个数的位置
	initializer_list<int>::iterator it2 = il.end();   //end()返回列表最后一个数的下一个位置
	cout << *it1 << endl;        //10
	cout << *(it2-1) << endl;    //30
	cout << il.size() << endl;   //3


    //所有容器都支持{ }的列表初始化,因为它们都增添了一个支持initializer_list的构造函数
    map<string, string> dict = { {"sort", "排序"}, {"left", "左边"} };

	// ps:以下是不同的初始化规则
	vector<Point> vp{ {1,2},{3,4} };  // 调用initializer_list的vector构造函数
	Point p1 = {1,1};                 // 直接调用两个参数的构造(隐式类型转换)
    

	return 0;
}

二、声明

1.关键字 auto

C++98中,auto是一个存储类型的说明符,用来表明变量是局部自动存储类型。但局部域中定义的局部变量默认就是自动存储类型,于是auto的用法显得有些多此一举,比较鸡肋。

而C++11废弃auto原来的用法,并使它可以支持变量类型的自动推导

cpp 复制代码
int main()
{
	int i = 10;
	auto p = &i;
	cout << typeid(p).name() << endl;  //int * __ptr64


    //1.通过auto可以简写繁琐的数据类型	
	map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
	auto it = dict.begin();  //map<string, string>::iterator it = dict.begin();


    //2.在一些情景下,auto可以帮助保存数据,防止数据丢失
	short a = 32670;
	short b = 32670;
	//short类型的数据范围为[-32768,32767],
	//a+b的结果应为65340,超出这个数据范围,
	//short类型的变量无法正确存储这个值 
	short c = a + b;	//用一个同为short类型的变量c来存储a+b的结果,存在与预期不符的隐患,可能导致数据丢失
	cout << c << endl;  //-196(结果与预期不符)
	auto d = a + b;		//通过auto去让编译器根据a+b的结果推导变量d的类型
	cout << d << endl;  //65340(结果符合预期)
	cout << typeid(d).name() << endl; //int(a+b发生了类型转化)
	

	return 0;
}

定义auto类型,要求必须对auto修饰的变量进行显示初始化,以便编译器将变量的类型设置为初始化值的类型。

(auto的更多用法细节,详见:【C++】从C到C++

2.关键字 decltype

与auto类似,decltype也可以自动推导类型。但与auto根据初始值推导变量的类型不同,decltype推导的是表达式结果的类型,更具体地说,decltype可以将变量的类型声明为表达式指定的类型

cpp 复制代码
template<class T1, class T2>
void Func(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}

int main()
{
	int x = 1;
	double y = 2.2;

    //1.decltype可以推导表达式指定的类型来定义变量
	decltype(x * y) ret;                //x * y的结果是double类型
	cout << typeid(ret).name() << endl; //double

	decltype(&x) p1;                    //&x的结果是int*类型
	cout << typeid(p1).name() << endl;  //int* __ptr64

	decltype(x) p2 = 1.25;              //x的类型是int
    cout << typeid(p2).name() << endl;  //int
	cout << p2 << endl;	                //1

    //2.也可以结合容器使用
	vector<decltype(x * y)> v;        x * y的结果是double类型
	cout << typeid(v).name() << endl; //class std::vector<double,class std::allocator<double> >

    //3.还可以结合模板使用
	Func(1, 'a');    //打印结果:int
	
return 0;
}

3.关键字 nullptr

如果一个指针没有合法的指向,一般会将它置为空指针。

C++98中,用NULL来表示空指针,是一个整型值0的宏替换。

cpp 复制代码
#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

但这样可能存在一些隐患,因为整型值0既可以是一个整型数字,也可以是无类型的指针 (void*) 常量,默认情况下,编译器会将它看成是一个整型数字,如果要将它按照指针方式来使用,就必须对它的类型进行强转。

cpp 复制代码
void f(int)
{
    cout << "f(int)" << endl;
}

void f(int*)
{
    cout << "f(int*)" << endl;
}

int main()
{
    f(0);           //f(int)
    f(NULL);        //f(int)
    f((int*)NULL);  //f(int*)

    return 0;
}

C++11中,引入了新的表示空指针的关键字nullptr来优化这一问题。

cpp 复制代码
void f(int)
{
    cout << "f(int)" << endl;
}

void f(int*)
{
    cout << "f(int*)" << endl;
}

int main()
{
    f(0);           //f(int)
    f(NULL);        //f(int)
    f(nullptr);     //f(int*)

    return 0;
}
cpp 复制代码
int main()
{
    //C++11中,nullptr与(void*)0所占的字节数相同
    cout << sizeof(nullptr) << endl;  //8
    cout << sizeof((void*)0) << endl; //8

    return 0;
}

为了提高代码的稳定性,在表示指针空值时最好使用nullptr。

三、范围for循环

C++98中,要遍历一个数组或容器,一般通过以下方式:

cpp 复制代码
void TestFor()
{
     int array[] = { 1, 2, 3, 4, 5 };
     for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
         array[i] *= 2;
 
     for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
         cout << *p << endl;
}

C++11中,基于迭代器实现了范围for循环。它可以自动迭代,自动判断结束,与普通for循环一样,可以对元素做赋值、修改、读取等操作,也可以用continue来结束本次循环、用break来跳出整个循环。

cpp 复制代码
void TestFor()
{
 int array[] = { 1, 2, 3, 4, 5 };
 for(auto& e : array)
     e *= 2;
 
 for(auto e : array)
     cout << e << " ";
 
 return 0;
}

四、STL中的一些变化

C++11中,STL增添了新容器、新接口、{ }列表初始化的构造函数、右值引用、可变参数模板、移动构造、移动赋值等。

本小节主要整理了一些新容器、新接口,{ }列表初始化的构造函数已在上文中涵盖,右值引用、可变参数模板、移动构造、移动赋值等将在下文中涵盖。

1.新容器(了解)

C++11中,STL主要增添了四个容器:array(静态数组)、forward_list(单向链表)、unordered_map、unordered_set。实际最有用的是unordered_map和unordered_set(详见:【C++】Unordered_map && Unordered_set),另外两个较为鸡肋,了解即可。

· array(静态数组)

cpp 复制代码
#include <array>
int main()
{
	int a1[10];
	array<int, 10> a2; //第二参数是数组大小

	cout << sizeof(a1) << endl;//40
	cout << sizeof(a2) << endl;//40
    //在空间大小上和普通数组没有区别

    //主要的区别在于array内部可以进行严格的越界检查
	a1[15] = 1;  // 转化成指针的解引用
	a2[15] = 1;  // operator[]函数调用,会进行内部检查
	
	//array诞生的初衷,是代替C的数组
	//但实际用vector可以达到相同的效果,而且每个空间都自动初始化了,这就显得array很多余
	vector<int> a3(10, 0);

	return 0;
}

· forward_list(单向链表)

forward_list与list的区别是,forward_list仅支持头插头删而不支持尾插尾删,还支持在当前位置之后插入元素,它的优势是可以为每个节点节省一个指针的空间。

2.新接口(了解)

C++11中,主要为每个容器增添了cbegin()、cend()、crbegin()、crend()这些接口,以区分普通迭代器和const迭代器。这些接口的返回值类型均为const_iterator。

但C++98提供的begin()和end(),其实也可以返回const_iterator,这使得C++11的这些新接口都属于是锦上添花,实际意义并不大。

尽管有一些新容器、新接口很容易被打上"鸡肋容器/接口大赏"的标签,让人不禁怀疑委员会一再拖更的C++11的含金量,但还请先相信委员会,请见谅博客小编的欲扬先抑,请先不要为"鸡肋容器/接口"捧腹,因为之前的内容都是开胃小菜,即将到达下文的是C++11真正的"正餐"。

五、右值引用和移动语义

1.表达式的值类别

表达式是由运算符和运算对象构成的计算式。一个表达式,例如a+b、x*y等,它们是可以求值的。对一个表达式求值往往能得到一个结果,而这个结果通常有两个属性:一个是值的类型 ,另一个是值的类别

值的类型例如int、double、char、string等。

C++98中,值的类别被分为了左值右值

· 左值

左值是一个表达式的值(例如:变量名、解引用的指针),它的特点是:

  1. 可以获取它的地址,也可以对它进行赋值;
  2. 可以出现在赋值"="的左右两边;
  3. const修饰的左值,不能赋值,仅可以取它的地址。
cpp 复制代码
int main()
{
    // 以下的a、*b、*str都是左值
    int a = 10;
    int *b = new int(10);
    const char *str = "hello world";

    return 0;
}

· 右值

右值也是一个表达式的值(例如:字面常量、表达式返回值,函数返回值),它的特点是:

  1. 无法获取它的地址,也无法对它进行赋值;
  2. 可以出现在赋值"="的右边,但不能出现在赋值"="的左边(因为右值无法被修改)。
cpp 复制代码
int main()
{
    double x = 1.1, y = 2.2;

    //10、x + y、fmin(x, y)都是右值
    10;
    x + y;
    fmin(x, y);

    //右值无法赋值修改,故以下会引发编译报错
    // error C2106: "=": 左操作数必须为左值
    //10 = 1;
    //x + y = 1;

    return 0;
}

【Tips】能直接区分左值和右值的方式:看一个值是否能取地址,能则是左值,不能则是右值。

2.左值引用和右值引用

C++98中已经有了引用,且几乎是左值引用。而右值引用是C++11中新增的。

引用就是给值取别名。左值引用就是给左值取别名,右值引用就是给右值取别名

cpp 复制代码
int main()
{
	double x = 1.1, y = 2.2;

	// 左值引用:给左值取别名
	int a = 0;
	int& r1 = a;

	// 左值引用能否给右值取别名?
    //int& r2 = 10;//编译报错(非常量引用的初始值必须为左值),不可
    //右值具有常性,直接对其左值引用涉及权限的放大
	// 但const左值引用可以
	const int& r2 = 10;
	const double& r3 = x + y;
	

	// 右值引用:给右值取别名
	int&& r5 = 10;
    //double& = x + y;//编译报错
    //【ps】右值引用的语法:&&
	double&& r6 = x + y;
	// 右值引用能否给左值取别名?
	//int&& r7 = a;//编译报错(无法从"int"转换为"int &&",无法将左值绑定到右值引用),不可
	// 但右值引用可以引用move后的左值(move()的作用是将左值强制转化为右值)
	int&& r7 = move(a);
    //虽然右值不能取地址,但是右值引用会使右值被存储到特定位置,且可以取到该位置的地址
    //也就是说,右值引用后的变量是左值

	return 0;
}

【小结】

· 关于左值引用:

  1. 普通的左值引用只能引用左值,不能引用右值;
  2. 但const修饰的左值引用,既可引用左值,也可引用右值。

· 关于右值引用:

  1. 右值引用只能右值,不能引用左值;
  2. 但右值引用可以引用move后的左值。

既然左值引用不仅可以引用左值,还可以引用右值,那么为什么C++11要新增右值引用?其实,新增的右值引用,与左值引用的缺陷有关。

3.左值引用的缺陷

引用的主要用途有:给变量取别名、作为函数的形参或返回值。引用给变量取别名,能够节省内存空间;而引用作为函数的形参或返回值,可以减少拷贝的工作量,提升效率。

同理,左值引用可以给变量取别名、作为函数的形参或返回值。而左值引用作为函数的形参或返回值,应该也可以减少拷贝的工作量,提升效率(且在往期博客中涉及的引用形参和引用作返回值也都是左值引用)。

这里,我们引入一个自定义的string类,来验证左值引用作为函数的形参或返回值的作用。

cpp 复制代码
namespace CVE
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

        //单参数的构造
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
            cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			memcpy(_str, str,_size+1);
		}

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

		// 拷贝构造 - 验证是否发生深拷贝
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;

			string tmp(s._str);
			swap(tmp);
		}
        

		// 赋值重载 - 验证是否发生深拷贝
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 赋值(深拷贝)" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

        //+=重载
        //string operator+=(char ch) 
		string& operator+=(char ch)
		{
			cout << "string& operator+=(char ch)" << endl;
			push_back(ch);
			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';
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

拷贝构造和赋值重载一般是以深拷贝的方式来完成的,拷贝的工作量较大。我们拷贝构造和赋值重载等函数中添加一个语句,使它们被调用时在屏幕上显示相关信息,可以被我们清楚捕获。

cpp 复制代码
//验证左值引用作为函数的形参或返回值的作用(见下图调试结果)

void func1(CVE::string s)        //一般的值作形参
{}

void func2(const CVE::string& s) //左值引用作形参
{}

int main()
{
	CVE::string s("hello world");

	// 通过func1和func2的调用可以看到
	// 左值引用做参数减少了拷贝
	func1(s);
	func2(s);

	// string operator+=(char ch) 传值返回存在深拷贝
	// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s += '!';

	return 0;
}

【结论】左值引用作为函数的形参和返回值都可以减少拷贝的工作量,提升效率。

但左值引用不能适用于以下这种场景。

cpp 复制代码
CVE::string& func()
{
	CVE::string str("xxxxxxx");
	//....
	return str;
}
int main()
{
	CVE::string ret = func();
    //func()被调用后,会开辟栈帧,str在栈帧上创建
    //str出func()的作用域后生命周期就结束,str会随栈帧一起被销毁
    //此时返回一个被销毁的变量的引用会有隐患
    //所以这里会用一个新的string对象来接收返回值,以防隐患
	//虽然编译器会优化这个过程,但先构造一个对象再对其进行赋值,
    //空间消耗的代价就大了

	return 0;
}

当函数返回的是一个局部的对象(这个局部对象出了函数作用域会被销毁了),就不能使用左值引用返回, 只能传值返回,否则会存在隐患。

尽管编译器会优化传值返回的过程,但传值返回会导致至少1次拷贝构造(如果是旧一点的编译器可能是两次拷贝构造),空间消耗大的问题仍存在。

【结论】当左值引用作为函数的返回值,返回的是局部对象,会导致非法访问(野指针),引发异常。

为了解决左值引用带来的问题,C++11不仅新增了右值引用,还基于右值引用新增了移动构造和移动赋值。

但在进入移动构造和移动赋值之前,有必要先来铺垫C++11中表达式的值类别。

4.值类别的再探讨

C++11对表达式的值类别进行了更细致的划分,将值类别分为:左值将亡值纯右值。其中,左值和将亡值合称为泛左值,纯右值和将亡值合称为右值。

· 纯右值

表达式的值具有以下任一特点即为纯右值:

  1. 表达式的值本身是纯粹的字面量(例如1、false、3.14等,但除了字符串字面量不是,例如"xxxx",它其实可以取地址,是一个左值);
  2. 表达式的值是一个匿名的临时对象
cpp 复制代码
int main()
{	
	//以下都是纯右值
	1;
	false;
	3.14;


	int a = 1, b = 2;

	//以下表达式都是纯右值表达式
	a + b;  //a + b得到的是匿名的临时对象
	a % b;
	a & b;
	a && b; //a && b的结果非true即false,相当于字面量
	a || b;
	!a;
	&a;		//&a得到的是a的地址,相当于unsigned int型的字面量


	int i = 0;

	++i;	 //i自增1后再赋值给i,最终返回i,结果是具名的。
	&(++i);	 //且可以取地址,++i的结果不是纯右值,是左值。
	i++;	 //i++是先对i进行一次拷贝,将这份拷贝返回,再对i自增1,
			 //i++的结果是对i加1前i的一份拷贝,是不具名的临时对象。
	//&(i++);//且不可取地址,i++的结果是纯右值
}

· 将亡值

在C++11之前,值的类别还没有被更细致地划分,那时C++98的右值其实与C++11中的纯右值等价。到了C++11,一类新的右值被引入,用于解决左值引用带来的问题,以及支持解决问题的方案------移动构造和移动赋值。

简单来说,将亡值就是即将被销毁的右值。以上文中用于解释左值引用的代码为例,其中func()就是一个将亡值表达式:

cpp 复制代码
CVE::string func()
{
	CVE::string str("xxxxxxx");
	//....
	return str;
}
int main()
{
	CVE::string ret = func();// func()的结果是一个匿名临时对象
    //验证:
	//&(func());             //编译报错:"&"要求左值。func()的结果无法取地址
	CVE::string&& s = func();//编译通过。func()的结果可以被右值引用
    //故 func()的结果是右值。
    //且由上文,func()的结果在完成对ret的赋值后会被销毁,
    //即出所在语句一行就被销毁,
    //故 func()的结果是将亡值。
	return 0;
}

这类新的右值是随着右值引用一起被引入的,常用于完成移动构造或移动赋值的任务。当完成任务后它会被马上销毁,这也是它的名字"将亡值"的由来。

一般而言,要区分纯右值和将亡值,可以从类型去判断------内置类型的右值就是纯右值,自定义类型的右值就是将亡值

5.移动(拷贝)构造和移动赋值

在C++11中,用左值去初始化一个对象或对一个已有进行对象赋值,会调用拷贝构造或赋值重载,先将要拷贝或赋值的资源创建成一个临时对象,再进行拷贝或赋值。

而用一个右值(包括纯右值和将亡值)去初始化或赋值,会调用移动(拷贝)构造或移动赋值,直接将资源转移(一般以交换的方式,这也属于浅拷贝的一种),从而避免深拷贝,提高效率。当该右值完成初始化或赋值的任务时,它的资源已经移动给了被初始化者或被赋值者,同时该右值也将会马上被销毁(一般通过析构)。

如下图所示,移动构造、移动赋值与一般的拷贝构造、赋值重载最大的不同在于,它们的形参是右值引用而不是const修饰的左值引用,以及它们不创建临时变量来完成拷贝或赋值。

移动构造、移动赋值不仅解决了左值引用带来的问题,可以避免非法访问,还可以在完成任务的基础上极大减少拷贝的工作量,提升效率。

这里, 我们在上文中用于验证左值引用的自定义的string类中,加入移动赋值,以验证右值引用和移动赋值的作用。

cpp 复制代码
namespace CVE
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			memcpy(_str, str, _size + 1);
		}

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

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;

			swap(s);
		}

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

			return *this;
		}

		//移动赋值
		string& operator=(string&& s)//右值赋值
		{
			cout << "string& operator=(string && s) -- 移动拷贝" << endl;
			swap(s);

			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)
		string& operator+=(char ch)
		{
			cout << "string& operator+=(char ch)" << endl;
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}
cpp 复制代码
//验证移动赋值的代码

CVE::string func()
{
	CVE::string str("xxxxxxx");
	//....
	return str;   //func()的结果(str)原本出作用域会被销毁
}
int main()
{
	CVE::string ret;
	ret = func(); 
    //这里,编译器做了特殊处理,将func()的结果(str)识别成将亡值
    //然后去调用移动赋值,实现资源转移

	return 0;
}

由下图调试结果可见,编译器调用了移动赋值函数,完成了资源转移。

所上图所示,移动赋值和赋值重载,因为函数名相同但参数不同的缘故,构成了函数重载。要使用二者中的哪一个来进行赋值,取决于编译器的选择。

cpp 复制代码
// 以下两个函数构成函数重载 
void func(const int& r)//const修饰的左值引用,既可引用左值,也可引用右值
{
	cout << "void func(const int& r)" << endl;
}

void func(int&& r)//右值引用
{
	cout << "void func(int&& r)" << endl;
}

int main()
{
	int a = 0;
	int b = 1;
	func(a);     //左值匹配左值引用
	func(a + b); //右值匹配右值引用
	// 编译器会区分左值/右值去选择更匹配的函数重载,
	// 且有右值引用的重载,就会优先走右值引用版本,
	// 不会出现调用歧义。
 
	return 0;
}

区分左值和右值的最大意义在于,能否进行移动构造或移动赋值。对于内置类型,左值引用和右值引用皆可;对于自定义类型,右值引用大大减少了拷贝的代价。

6.右值引用的价值

能够实现直接将资源转移,要归功于右值引用。

要说明右值引用的价值,不妨先将"变量接收返回值"与"右值引用接收返回值"做个比较。

如下图1所示,func1()的返回值是被相同类型的变量接收的。从监视窗口可以看到,变量a和变量b的地址不同,而在实际的调试过程中,图1中变量b比a先创建,出函数作用域后a被销毁,由此说明,b是a的拷贝。

如下图2所示,func2()的返回值是被右值引用接收的。从监视窗口可以看到,变量a和变量b的地址相近, 而在实际的调试过程中,变量b并没有被先创建,而是等变量a被创建后才被创建。

【Tips】右值引用会使右值被存储到特定位置且可以取到右值的地址 ,由此它可以支持移动构造、移动赋值实现对局部变量、临时对象资源的直接转移(通过右值引用作形参)。

右值引用的价值是减少拷贝,同时也弥补了左值引用的缺陷。它主要有两个应用场景:

  1. 自定义类型中深拷贝的类,必须传值返回;
  2. 对于容器的插入接口,如果插入对象是右值,就可以利用移动构造转移资源给数据结构中的对象,以减少拷贝的工作量。

C++11中,右值引用作形参被广泛应用于STL,且不仅被应用于移动构造和移动赋值,还应用于容器的接口。

7.右值引用的属性和原理

右值引用不仅可以单独作形参来减少拷贝的工作量,还可以和模板结合使用。

cpp 复制代码
template<typename T>
void PerfectForward(T&& t)
{}

右值引用结合模板作形参,既能接收左值,又能接收右值 。但其实模板中的"&&"不代表右值引用,而代表的是万能引用

模板的万能引用为函数提供同时接收左值引用和右值引用的功能。当所传实参是左值,万能引用就相当于左值引用;当所传实参是右值,万能引用就相当于右值引用。

尽管万能引用听上去十分强大,但它却存在弱点。

这里,我们通过以下代码来测试万能引用:

cpp 复制代码
//以下函数构成函数重载
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; }

//模板中的右值引用------------万能引用:既可以接收左值,也可以接收右值
//所传实参是左值,它就是左值引用(这个过程或称发生了引用折叠:T&& -> T&);
//实参是右值,它就是右值引用。
//这是结合模板的实例化实现的,编译器会根据实参的类型自动推导出
//与之匹配的形参类型,去调用相应的函数
template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}


int main()
{
    PerfectForward(10);           // 传右值
    int a;
    PerfectForward(a);            // 传左值
    PerfectForward(std::move(a)); // 传右值
    const int b = 8;
    PerfectForward(b);            // 传const修饰的左值
    PerfectForward(std::move(b)); // 传const修饰的右值
    return 0;
}

根据上文中的理论,当所传实参是左值,万能引用就相当于左值引用;当所传实参是右值,万能引用就相当于右值引用。那么,传左值就应该正确调用左值引用作形参的函数,传右值就应该正确调用右值引用作形参的函数,传const修饰的左值和右值同理。但实际结果并不如此:

调试结果显示, 传左值和传const修饰的左值调用了左值引用和const左值引用,而传右值和传const修饰的右值也调用了左值引用和const左值引用。

当所传实参是左值,万能引用可以作左值引用,而这个过程会发生引用折叠(即T&& -> T&)。那以上的调试结果是否与引用折叠有关呢?我们将与左值相关的传参全部屏蔽,仅测试右值传参,结果却和刚刚一样,传右值和传const修饰的右值调用的还是左值引用和const左值引用。

如果和引用折叠无关,那又是什么原因呢?

我们继续看下面这段代码:

cpp 复制代码
//r是左值?还是右值?
//rr是左值?还是右值?
int main()
{
	int a = 0;
	int& r = a;
	int&& rr = move(a);
	cout << &r << endl;
	cout << &rr << endl;

	return 0;
}

由调试结果,变量r和变量rr都可以取地址,且它们的地址是相同的,说明它们看似一个是左值一个是右值,但其实都是左值。 也就是说,右值引用的属性其实不是一个右值,而是一个左值。且由下面这段代码,右值引用其实也是支持修改的:

cpp 复制代码
int main()
{
	int a = 0;
	int&& rr = move(a);//move(a)是一个将亡值
	rr++;
	cout << rr << endl;
    
    int&& rrr=10;     //10是一个纯右值
    rrr++;
    cout << rrr << endl;

	return 0;
}

右值是不可取地址的,也是不可修改的。如果右值引用变量的属性是右值,那右值引用变量也应该具备右值的属性。但由刚刚的两个调试结果,右值引用的属性明显不是一个右值,而是一个左值。这是为什么呢?

在此,我们回顾上文中的移动赋值和相应的测试代码,来进一步说明这个问题。

右值引用变量的属性会被编译器识别成左值,否则在移动赋值的场景下无法完成资源转移。

【结论】右值引用变量的属性是一个左值,支持修改、支持取地址。

这其中的原因涉及引用的原理。引用的底层是通过指针来实现的,右值引用能够使右值被存储到一个特定位置,然后通过一个右值引用变量来指向这个特定位置。在这个特定位置上的右值是不可修改、不可取地址的,但指向这个特定位置的变量是可以修改、可以取地址的。也就是说,右值引用本质上是在右值上套了一层左值的外壳,以方便管理这个右值而右值一旦经过右值引用,就可以取到地址,此时编译器就会把它识别成左值了

这就是上文测试万能引用的代码中,传右值和传const修饰的右值调用的是左值引用和const左值引用的原因。

而想要保持右值原本的属性,就得通过std::forward()来实现完美转发。

8.完美转发与std::forward()

完美转发是指,一个函数或类模板可以将其参数原封不动地传给另一个函数或类模板,同时保持所传参数的左右值特性。 它在实现泛型编程中十分有用,既可以避免重复编写代码,又可以同时提高代码的复用性。而要实现完美转发,就要通过通过库函数std::forward()

std::forward()是一个C++11 中的模板函数 **,它能够在传参过程中保留数据的原生类别,**将一个参数以"原样"的方式传给另一个函数。该函数一般被用于实现完美转发。

cpp 复制代码
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<typename T>
void PerfectForward(T&& t)
{
	// 完美转发
    // t是左值引用,就保持左值属性
	// t是右值引用,就保持右值属性
    Fun(std::forward<T>(t));
}

int main()
{
	PerfectForward(10);           // 传右值
	int a;
	PerfectForward(a);            // 传左值
	PerfectForward(std::move(a)); // 传右值
	const int b = 8;
	PerfectForward(b);            // 传const修饰的左值
	PerfectForward(std::move(b)); // 传const修饰的右值
	return 0;
}

在测试万能引用的代码中加入std::forward()后,无论是传左值还是传右值都可以正确调用相应的函数了。

六、新的类功能

1.新的默认成员函数

C++98中,一个类拥有六个默认成员函数,它们分别是:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const 取地址重载。 最后重要的是前4个,后两个用处不大。默认的意思是,用户自己不写编译器就会默认生成。

C++11中新增了两个默认成员函数:移动构造函数、移动赋值运算符重载。

它们的使用有以下注意事项:

  1. 如果用户自己没有实现移动构造,也没有实现析构 、拷贝构造、赋值重载中的任 意一个 ,那么编译器会自动生成一个默认的移动构造对于内置类型成员,默认生成的移动构造会将每个成员按字节进行拷贝;对于自定义类型成员,则需要先看用户自己是否为这个成员实现了相应的移动构造,实现了就去调用相应的移动构造,没有实现就去调用拷贝构造。
  2. 如果用户自己没有实现移动赋值,也没有实现析构 、拷贝构造、赋值重载中的任意一个 ,那么编译器会自动生成一个默认的移动赋值对于内置类型成员,默认生成的移动赋值会将每个成员按字节进行拷贝;对于自定义类型成员,则需要先看用户自己是否为这个成员实现了相应的移动赋值,实现了就去调用相应的移动赋值,没有实现就去调用赋值重载。
  3. 如果用户自己实现了移动构造或者移动赋值,那么编译器就不会自动生成拷贝构造和赋值重载。
cpp 复制代码
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:
	string _name;
	int _age;
};

int main()
{    
    //用户自己没有实现移动构造或移动赋值,
    //也没有实现析构 、拷贝构造、赋值重载中的任意一个
    //那么编译器会自动生成默认的
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

2.强制生成默认成员函数 - 关键字default

C++11可以让用户更好地控制要使用的默认成员函数。

假设要使用某个默认成员函数,但因为一些原因编译器没有自动(例如用户自己实现了拷贝构造,那编译器就不会生成移动构造了)那就可以使用default关键字显示地指定编译器生成默认的移动构造。

cpp 复制代码
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;

private:
	string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

3.禁止生成默认成员函数 - 关键字delete

如果想要限制编译器生成默认成员函数,在C++98中,是将该函数设置成私有(private),且要声明补丁。但这样做,其他人想要调用的话就会报错。

在C++11中更简单,只需使用delete关键字显示地指定编译器不生成成员函数的默认版本,且称delete修饰的成员函数为删除函数。

cpp 复制代码
class Person
{
public:
	//构造
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	//指定不生成拷贝构造
	Person(const Person& p) = delete;

private:
	string _name;
	int _age;
};
int main()
{
	Person s1;

	//编译报错:无法引用函数Person::Person(const Person& p)--它是已删除的函数
	//Person s2 = s1;
	//Person s3 = std::move(s1);

	return 0;
}

4.缺省值初始化类成员变量

C++11中,支持在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值来对成员变量初始化。

(详见本小节内容,请移步至:【C++】从C到C++

5.重写相关 - 关键字final和override

C++11中,新增了两个关键字final、override来检查虚函数重写。

(详见本小节内容,请移步至:【C++】多态

七、可变参数模板

C++98中,类模版和函数模版中只能含固定数量的参数。C++11新提供了可变参数模板,支持函数模板和类模板的参数数量可变。例如打印函数printf()就是一个典例:

cpp 复制代码
printf("%d",x);
printf("%d %d",x,y);
printf("%d %d %d",x,y,z);

​​​​

函数参数一般为一个个对象,可变函数参数意味着可以传多个对象;模板参数一般为一个个类型 ,同理,可变模板参数意味着可以传多个类型。

例如以下就是一个可以传多个类型的函数模板:

cpp 复制代码
//声明一个参数包Args ...  args,这个参数包可以包括0到任意个模板参数.
template<class ...Args>        //Args是一个模板参数包
void ShowList(Args ...  args)  //args是一个函数形参参数包
{}

可变模版参数的标志是,参数名前带有"..." ,例如args前面有"...",所以args就是一个可变模版参数。带"..."的参数又称作参数包,其中包含了0到任意个模版参数

cpp 复制代码
template <class ...Args>    //形参的类型
void ShowList(Args... args) //形参包
{
	cout << sizeof...(args) << endl;//显示形参类型的个数

	// 并不支持这样
	/*for (size_t i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << endl;
	}*/
}
int main()
{
    string str = "hello";
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', str);
}

适用情景例如:

cpp 复制代码
​
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date构造" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date拷贝构造" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
//可变参数模板
template <class ...Args>
Date* Create(Args... args)
{
	Date* ret = new Date(args...);

	return ret;
}
int main()
{
	Date* p1 = Create();
	Date* p2 = Create(2023);
	Date* p3 = Create(2023, 9);
	Date* p4 = Create(2023, 9, 27);

	Date d(2023, 1, 1);
	Date* p5 = Create(d);
 
	return 0;
}

​

参数包中的每个参数是无法直接获取的,只能通过展开参数包(即展开可变模版参数)的方式来获取其中的每个参数

1.展开参数包

· 递归函数方式

在参数包前增加一个模板参数(下文代码中为T),使每次参数包接收实参的传入时,参数包中的第一个参数总是T。然后,将这第一个参数取出,剩余的参数再作为一个新的参数包传给自身,这个过程类似于递归调用。当参数包里没有剩余参数的时候,会根据第一个参数自动调用合适的函数。

cpp 复制代码
//递归函数方式
template <class T>
void ShowList(T val)// 作为递归结束条件的函数
{
	cout << val << " ";
	cout << endl;
}
template <class T, class ...Args>
void ShowList(T val, Args... args)
{
    cout << val << " ";
	ShowList(args...);//接收剩下的参数包递归调用
}
int main()
{
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 2.2);

	// ...

	return 0;
}
cpp 复制代码
//当类型只有参数包,先封装一层再调用
void _ShowList()// 作为结束条件的函数
{
	cout << endl;
}
template <class T, class ...Args>
void _ShowList(T val, Args... args)
{
	cout << val << " ";
	_ShowList(args...);//接收剩下的参数包递归调用
}
template <class ...Args>
void CppPrint(Args... args)
{
	_ShowList(args...);//args代表类型个数为0或N的参数包
}

int main()
{
	CppPrint();
	CppPrint(1);
	CppPrint(1, 2);
	CppPrint(1, 2, 2.2);
	CppPrint(1, 2, 2.2, string("xxxx"));

	// ...

	return 0;
}

· 逗号表达式方式

这种展开参数包的方式,是直接在一个带参数包的函数的函数体中展开,例如下文代码中的CppPrint(),而CppPrint()函数体中的PrintArg()用于处理参数包中每一个参数。

这种就地展开参数包的方式实现的关键是逗号表达式。逗号表达式会按顺序执行逗号左边的表达式, 而它的结果是逗号右边的值。CppPrint()中的逗号表达式:(PrintArg(args), 0),也是按照这个执行顺序,先执行 PrintArg(args),再得到逗号表达式的结果0。

这里还利用了{ }列表初始化来初始化一个变长数组, {(PrintArg(args), 0)...}将会展开成((PrintArg(arg1),0), (PrintArg(arg2),0), (PrintArg(arg3),0))......最终会创建一个元素值都为0的数组int arr[sizeof(Args)]。在创建数组的过程中,会先执行逗号表达式的左边,去调用PrintArg() 打印参数,也就是说,在创建整型数组a[ ]的过程中,参数包也随之展开了。而这个整型数组a[ ]的功能纯粹是为了在数组创建之际同时展开参数包。

cpp 复制代码
void CppPrint()//0个参数
{
	cout << endl;
}

template <class T>
void PrintArg(T t)//N个参数
{
	cout << t << " ";
}

​template <class ...Args>
void CppPrint(Args... args)
{
	int a[] = { (PrintArg(args),0)...};//通过一个函数来获取参数包
    //编译器会自动推导参数包中的参数个数
    //数组通过{}中的值来开辟数组的空间
    //(PrintArg(args),0)是一个逗号表达式,逗号右边的值是表达式的值,
    //该式通过0来对数组初始化
	cout << endl;
}

int main()
{
	CppPrint();
	CppPrint(1);
	CppPrint(1, 2);
	CppPrint(1, 2, 2.2);
	CppPrint(1, 2, 2.2, string("xxxx"));

	return 0;
}
cpp 复制代码
//或如下简化,不用逗号表达式
void CppPrint()
{
	cout << endl;
}

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

	return 0;
}

//args代表0-N的参数包
template <class ...Args>
void CppPrint(Args... args)
{
	int a[] = { PrintArg(args)...};
	cout << endl;
}
int main()
{
	CppPrint();
	CppPrint(1);
	CppPrint(1, 2);
	CppPrint(1, 2, 2.2);
	CppPrint(1, 2, 2.2, string("xxxx"));

	return 0;
}

//借助CppPrint()中的数组a[]多次调用PrintArg(),每次调用后返回值是0
//借助这多个0来初始化数组a[]

2.emplace系列接口

C++11基于可变参数模板在STL中提供了emplace系列接口,主要与插入操作相关。

emplace系列接口都是模板函数,它们既是万能引用模板,也是可变参数模板,无论数据的类别是左值还是右值,无论数量是多少,它们都可以处理。

以list为例,emplace()的作用和insert()类似,emplace_back()的作用和push_back()类似,emplace_front()的作用和push_front()类似 。不仅list支持了emplace系列接口,其他STL容器同样也支持。

下面,我们来测试emplace_back和push_back的区别。

首先是对于内置类型的:

cpp 复制代码
int main()
{
	std::list< std::pair<int, char> > mylist;

	// emplace_back()支持可变参数,拿到构建pair对象的参数后自己去创建对象
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');
	mylist.emplace_back(make_pair(30, 'c'));

    // 但对于内置类型,除了用法上,和push_back()并没什么太大的区别
	mylist.push_back(make_pair(40, 'd'));
	mylist.push_back({ 50, 'e' });

	for (auto e : mylist)
		cout << e.first << ":" << e.second << endl;

	return 0;
}

这里,我们再将上文中测试左值引用的自定义的string类拿过来,用于测试emplace_back和push_back对于自定义类型的区别。

cpp 复制代码
//自定义的string类
namespace CVE
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str)" << endl;

			_str = new char[_capacity + 1];
			memcpy(_str, str, _size + 1);
		}

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

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;

			swap(s);
		}

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

			return *this;
		}

		//移动赋值
		string& operator=(string&& s)//右值赋值
		{
			cout << "string& operator=(string && s) -- 移动拷贝" << endl;
			swap(s);

			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)
		string& operator+=(char ch)
		{
			cout << "string& operator+=(char ch)" << endl;
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}
cpp 复制代码
//测试emplace系列接口

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date构造" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date拷贝构造" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main() 
{
	std::list< std::pair<int, CVE::string> > mylist;
	std::pair<int, CVE::string> s;
	cout << "********" << endl;
	mylist.emplace_back(s);
	cout << "********" << endl;
	mylist.push_back(s);

	cout << "------------------" << endl;

	mylist.emplace_back(10, "sort");
	cout << "********" << endl;
	mylist.push_back(make_pair(30, "sort"));
	// CVE::string带有拷贝构造和移动构造
	// emplace_back和push_back其实差别也不大
	// emplace_back是直接构造了,
	// push_back是先构造,再移动构造,其实也还好。

	cout << "------------------" << endl;

	std::list<Date> lt;
	Date d(2023, 9, 27);
	cout << "********" << endl;
	// 只能传日期类对象
	lt.push_back(d);

	cout << "------------------" << endl;

	// 可以传日期类对象
	// 也可以传日期类对象的参数包
	lt.emplace_back(d);
	lt.emplace_back(2023, 9, 27);

	return 0;
}

对于有移动构造的自定义类型(代码中的string类),插入左值时,emplace_back和push_back没有区别,都会进行深拷贝;插入右值时,emplace_back仅调用了构造函数,push_back调用了构造函数和移动构造;push_back不支持插入多个值,而emplace_back插入多个值时也仅调用了构造函数。

在插入右值时,emplace_back比push_back少调用了一个移动构造。移动构造是将右值的资源进行转移,本身十分高效的,拷贝代价很小。也就是说,在有移动构造的时候,只有插入自定义类型的右值,emplace_back的效率才比push_back略高。

对于没有移动构造的自定义类型(代码中的Date类),emplace_back插入左值,调用了拷贝构造,发生深拷贝;插入右值,则仅调用了构造函数,不发生深拷贝。

emplace_back因为支持多个参数,能比push_back少调用拷贝构造,不发生深拷贝,从而降低开销提高效率。在没有移动构造的时候,emplace_back会比push_back高效许多。

结论总得来说,emplace_back()的用法比push_back()更丰富,效率上也更高效一些。

八、lambda表达式

1.仿函数的劣势

实际进行数据管理时,经常离不开排序。

在C++98中,要对一个数据集合中的元素进行排序,可以使用std::sort()。

cpp 复制代码
#include <algorithm>

int main()
{

    int array[] = {4,1,8,5,3,7,0,9,2,6};

    // sort()默认按照小于比较,排出来结果是升序
    std::sort(array, array+sizeof(array)/sizeof(array[0]));

    // 如果需要降序,需要通过仿函数改变元素的比较规则
    std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
    //greater<int>()就是一个仿函数
    //ps:greater<int>后面的()
    //如果传入的是对象,就需要加上()(这里会构造一个匿名对象)
    //如果传入的是类型,就不需要加(),例如:"priority_queue<int, vector<int>, greater<int>>;"


    return 0;
}

对于内置类型的数据,想要改变数据元素的比较规则,可以使用库中提供的仿函数(或称函数对象);对于自定义类型的数据,就需要用户自己定义排序的比较规则了。

cpp 复制代码
//定义一个商品类,成员属性有商品名,商品价格,商品评级
#include<algorithm>

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价

	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

struct CompareEvaluateGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._evaluate > gr._evaluate;
	}
};

void Print(vector<Goods>& v)
{
	for (auto& e : v)
	{
		cout << "商品名:" << e._name << " 价格:" << e._price << " 评级:" << e._evaluate << endl;
	}
}
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3.1, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };

	cout << "初始顺序:" << endl;
	Print(v);

	sort(v.begin(), v.end(), ComparePriceLess());        // 价格升序
	cout << "价格升序:" << endl;
	Print(v);

	sort(v.begin(), v.end(), ComparePriceGreater());     // 价格降序
	cout << "价格降序:" << endl;
	Print(v);

	sort(v.begin(), v.end(), CompareEvaluateGreater());  // 评价的降序
	cout << "评价的降序:" << endl;
	Print(v);

	return 0;
}

对于一个简单情景来说,例如仿函数是十分方便且功能十分强大的。但当代码量大的时候,使用仿函数来改变比较规则就有一些劣势:

  1. 每一次不同规则的比较,都需要实现一个仿函数类,代码较为的冗余。
  2. 不同的人对管理不同比较规则的仿函数的命名也不同,查看时较为繁琐和不易理解。且当仿函数数量较多时,仿函数的命名也是一个问题(如以下代码)。
cpp 复制代码
#include<algorithm>

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价

	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

//struct ComparePriceLess
struct Compare1
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

//struct ComparePriceGreater
struct Compare2
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

struct CompareEvaluateGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._evaluate > gr._evaluate;
	}
};

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3.1, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };

	sort(v.begin(), v.end(), Compare1());  // 价格升序

	sort(v.begin(), v.end(), Compare2());  // 价格降序

    // 直接看这两个仿函数的命名:Compare1()和Compare2()
    // 难以得知它们管理着什么比较规则


	return 0;
}

为了实现一个排序算法,每次都要重新去定义一个类,且只要每次的比较规则有所不同,就得去实现多个类。还有类的命名问题, 这些都带来了许多不便。因此,在C++11中出现了Lambda表达式。

2.lambda表达式的运用

lambda表达式,或称lambda函数,代码量小,适用于管理小型的比较规则。

【补】管理小型的比较规则:

  1. 函数指针 void(*ptr)(int x) - 能不用就不用
  2. 仿函数(类中重载operator()) - 类对象可以像函数一样使用
  3. lambda 匿名的函数对象 - 在函数内部直接定义和使用

不同于仿函数的定义形式,lambda表达式可以直接在函数内部定义和使用,且效果与仿函数相当。以上文中商品类的代码为例:

cpp 复制代码
#include<algorithm>

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _evaluate; // 评价

	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};

struct CompareEvaluateGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._evaluate > gr._evaluate;
	}
};
void Print(vector<Goods>& v)
{
	for (auto& e : v)
	{
		cout << "商品名:" << e._name << " 价格:" << e._price << " 评级:" << e._evaluate << endl;
	}
}
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3.1, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
	cout << "初始顺序:" << endl;
	Print(v);

	//仿函数控制比较规则
	sort(v.begin(), v.end(), ComparePriceLess());        // 价格升序
	sort(v.begin(), v.end(), ComparePriceGreater());     // 价格降序
	sort(v.begin(), v.end(), CompareEvaluateGreater());  // 评价的降序

	//lambda表达式控制比较规则
	sort(v.begin(), v.end(), [](const Goods& x, const Goods& y) {
		return x._price < y._price; });// 价格升序
	cout << "价格升序:" << endl;
	Print(v);

	sort(v.begin(), v.end(), [](const Goods& x, const Goods& y) {
		return x._price > y._price; });// 价格降序
	cout << "价格降序:" << endl;
	Print(v);

	sort(v.begin(), v.end(), [](const Goods& x, const Goods& y) {
		return x._evaluate > y._evaluate; });// 评价的降序
	cout << "评价的降序:" << endl;
	Print(v);

	return 0;
}

【Tips】lambda表达式的定义细节:

[捕捉列表](参数列表)mutable->(返回值类型){函数体}

  • [捕捉列表]:它总是出现在lambda表达式的开始位置 ,是编译器判断lambda表达式的依据(所以必须写[ ],且[ ]内可以有参数 ),能够捕捉上下文中的变量供lambda表达式使用;
  • (参数列表):它和普通函数的参数列表一样,如果不需要参数传递,可以连同( )一起省略;
  • mutable:mutable是一个修饰符(中文译为可变的),一般情况下都是省略不写的,使用时需自行给出参数列表的形参默认都是被const修饰的 ,形参不可以被修改,而mutable可以取消形参的常量属性使用mutable时,参数列表不可省略(哪怕不需要参数传递)
  • ->返回值类型:->和返回值类型是一体的(例如->int表示lambda的返回值类型是int),一般来说,没有返回值或{函数体}有确定的返回值类型,->返回值类型可以省略不写(这是因为编译器可以根据{函数体}中的return推导出返回值类型);
  • {函数体}:和普通函数一样,{ }里的是lambda表达式的功能主体。{函数体}除了可以使用参数列表中的参数,也可以使用捕捉列表中的参数(如果捕捉列表有捕捉参数的话)。
cpp 复制代码
//基本说明:
int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};

	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=] {return a + 3; };

	// 省略了返回值类型,无返回值类型
	auto fun1 = [&](int c) {b = a + c; };
	fun1(10);
	cout << a << " " << b << endl;

	// 各部分都很完善的lambda表达式
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;

	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}
cpp 复制代码
void func()
{
	cout << "func()" << endl;
}
int main()
{
	auto ret = [](int x, int y)->int { return x + y; };//[ ]能够捕捉上下文中的变量,[ ]内可以有参数但一般没有
	//auto ret = [](int x, int y){ return x + y; };    //虽然"->返回值类型"可省,但建议写上,增加代码的可读性
	cout << typeid(ret).name() << endl;				   //编译器可以根据{函数体}中的return推导出返回值类型
	cout << ret(3, 4) << endl;						   //像一个局部的函数,但实际上是一个具有函数功能的对象
	cout << ret(4, 5) << endl;


	auto add1 = [](int x, int y)
		{
			//cout << ret(a, b) << endl;//内部不能直接调用其他局部的lambda表达式
			func();//但可以直接调用全局的函数
		};
	add1(1, 2);
	//可以通过捕捉的方式调用局部的表达式
	int a = 1, b = 2;
	auto add2 = [ret](int x, int y)
		{
			cout << ret(x, y) << endl;
		};
	add2(a, b);

	return 0;
}
cpp 复制代码
//交换
int main()
{
	//一般写法交换x和y
	int x = 0, y = 1;
	auto swap1 = [](int& t1, int& t2)->void
		{
			int tmp = t1;
			t1 = t2;
			t2 = tmp;
		};
	swap1(x, y);
	cout << "x:" << x << " y:" << y << endl;

	//[ ]中传参交换x和y 
	x = 0, y = 1;
	auto swap2 = [x, y](int& t1, int& t2)->void
		{
			int tmp = t1;
			t1 = t2;
			t2 = tmp;
		};
	swap2(x, y);
	cout << "x:" << x << " y:" << y << endl;

	//以上两种写法的结果是一样的

	//

	//捕捉列表既可以传值捕捉,也可以传引用捕捉

	//传值捕捉
	x = 0, y = 1;
	auto swap3 = [x, y]()mutable //mutable可以取消形参的常性,使形参可以修改
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap3();
	cout << "x:" << x << " y:" << y << endl;
	// 但形参的改变不影响实参
	// 虽然mutable让捕捉的x和y可以改变了,但是它们依旧是表达式之外的x和y的拷贝

	//传引用捕捉
	x = 0, y = 1;
	auto swap4 = [&x, &y]()
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap4();
	cout << "x:" << x << " y:" << y << endl;

	return 0;
}
cpp 复制代码
//捕捉列表除了传值捕捉和传引用捕捉
//还提供了全捕捉和混合捕捉

//全部引用捕捉
auto func1 = [&](){};
//全部传值捕捉
auto func2 = [=](){};

//混合捕捉:x引用捕捉,y传值捕捉
auto func3 = [&x, y](){};
//混合捕捉:其他参数引用捕捉,x传值捕捉
auto func4 = [&,x](){};
//混合捕捉:其他参数传值捕捉,x引用捕捉
auto func5 = [=, &x](){};
cpp 复制代码
int main()
{
	int a = 1, b = 1, c = 1;

	auto f1 = [=]()		 //全捕捉
		{
			cout << a << " " << b << " " << c << endl;
		};
	f1();

	auto f2 = [=, &a]() //混合捕捉
		{
			a++;
			cout << a << " " << b << " " << c << endl;
		};
	f2();

	return 0;
}

【ps】lambda表达式的定义

  1. 定义lambda表达式时,参数列表和->返回值类型都可省,可写可不写;
  2. 但建议写上"->返回值类型",增加代码的可读性;
  3. 捕捉列表和函数体不可省,但是其内容可以为空(因此最简单的lambda表达式为:"[ ]{ }; ",它没有任何功能);
  4. 捕捉列表既可以传值捕捉,也可以传引用捕捉,同时还支持全捕捉和混合捕捉。
    【ps】捕捉列表的更多说明
  • [var]:表示值传递方式捕捉变量var
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示引用传递捕捉变量var
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
  • [this]:表示值传递方式捕捉当前的this指针

注:

  1. 父作用域指包含lambda表达式的语句块。
  2. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
  3. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
  4. 在块作用域以外的lambda函数捕捉列表必须为空。
  5. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者 非局部变量都 会导致编译报错。
  6. lambda表达式之间不能相互赋值,即使看起来类型相同
cpp 复制代码
void (*PF)();

int main()
{
	auto f1 = [] {cout << "hello world" << endl; };
	auto f2 = [] {cout << "hello world" << endl; };

    //lambda表达式之间不可以赋值

    //f1 = f2;   //编译失败--->提示找不到operator=()

	// 但可以使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();

	// 也可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();

	return 0;
}

3.lambda表达式的大小

lambda表达式的大小分为三种情况:

1)无捕捉

如果lambda表达式的捕捉列表没有参数,就和仿函数一样,大小为1

cpp 复制代码
//无捕捉
int main()
{
	int a = 1, b = 2;
	auto ret = [](int a, int b) {return a + b; };
	cout << sizeof(ret) << endl;//1

	return 0;
}

2)全捕捉

全捕捉的情景下,编译器并不会为所有捕获的参数都开辟空间,而只为函数体中使用过的参数开空间

cpp 复制代码
//全传值
int main()
{
	int a, b, c, d, e;
	auto ret1 = [=]()mutable
		{
			a = b = c = 0;
		};
	cout << sizeof(ret1) << endl;//12

	auto ret2 = [=]()mutable
		{
			a = b = c = d = e = 0;
		};
	cout << sizeof(ret2) << endl;//20

	return 0;
}
cpp 复制代码
//全传引用(32位平台下)
int main()
{
	int a, b, c, d, e;
	auto ret1 = [&]()mutable
		{
			a = b = c = 0;
		};
	cout << sizeof(ret1) << endl;//12

	auto ret2 = [&]()mutable
		{
			a = b = c = d = e = 0;
		};
	cout << sizeof(ret2) << endl;//20

	return 0;
}

3)混合捕捉

混合捕捉的规则是,捕获的变量和函数体中使用过的变量,编译器都会为它们开空间

cpp 复制代码
int main()
{
	int a, b, c, d, e;
	//全传值,个别传引用
	auto ret1 = [=,&d,&e]()mutable
		{
			a = b = c = 0;
		};
	cout << sizeof(ret1) << endl;//20

	//全传引用,个别传值
	auto ret2 = [&,d,e]()mutable
		{
			a = b = c  = 0;
		};
	cout << sizeof(ret2) << endl;//20

	return 0;
}

4.lambda表达式的原理

上文中已经提过,lambda表达式的效果与仿函数相当,而形式更加简洁明了。从两者管理商品类的比较规则不难看出:当功能类似时,对于在主函数中使用的形式以及各自的函数体,lambda表达式和仿函数完全一样;而当数据一样时,对于排序的结果,lambda表达式和仿函数也是完全一样的。

那么,lambda表达式的实现是否跟仿函数有关呢?

下面我们再通过一个例子将两者加以比较:

cpp 复制代码
//计算利率
class Rate
{
public:
	Rate(double rate) : _rate(rate)
	{}
	double operator()(double money, int year)
	{
		return money * year * _rate;
	}
private:
	double _rate;
};
 
int main()
{
	// 仿函数
	double rate = 0.5;
	Rate r1(rate);
	cout << r1(518, 3) << endl;
 
	// lambda
	auto r2 = [=](double money, int year)->double {return money * year * rate;};
	cout << r2(518, 3) << endl;
 
	return 0;
}

在以上代码中,rate是一个double变量,仿函数使用它时通过Rate类提供的构造函数初始化Rate类中的成员变量_rate,进而调用operator();而lambda通过捕捉列表来捕获这个double变量。

同样可以直观地看出,当功能类似时,对于在主函数中使用的形式以及各自的函数体,lambda表达式和仿函数完全一样;而当数据一样时,对于排序的结果,lambda表达式和仿函数也是完全一样的。

而通过它们汇编代码不难发现,它们的实现过程是相似的:

仿函数创建对象时,编译器调用了Rate类域中的构造函数;调用这个对象时,编译器调用了Rate类域中的operator()成员函数。相似的,lambda表达式也是调用了一个类中的构造函数和operator(),只是这个类的类名不像仿函数调用的Rate类那样明确,而是一串16进制的数字组成。这串16进制的数字(一般有32个)又叫UUID,是通过一套算法生成的,且绝不重复,用来唯一地标识每个lambda表达式。也就是说,lambda表达式所调用的类其实是编译器自己生成的,这个生成的类的类名可以简单看作lambda_UUID(这也解释了为什么lambda表达式之间不能赋值,因为UUID的不同,每个lambda表达式的类型其实是不同的)。

【结论】 在底层,编译器会为每个lambda表达式生成一个独立的类,且在该类中也重载了operator(),通过与仿函数类似的方式来实现表达式的功能。所以,lambda表达式的原理与仿函数类似,它本质上其实是一个匿名的仿函数(或称函数对象)。

九、包装器

1.引例

function包装器,也称为适配器。它的功能是什么,为什么需要它呢?要回答这些问题,首先要经过这样一个例子:

cpp 复制代码
ret = func(x);

以上代码中,func()也许会被直观地以为是一个函数,但实际上,func()的字面含义十分丰富,它可能是一个函数名,可能是一个函数指针,也可能是一个仿函数对象,甚至可能是lambda表达式。

这样一个简单的"func()"涉及了如此多可调用的类型,而当它需要跟模板结合使用的时候,可能会导致模板实例化的效率低下。

例如下面这个例子:

cpp 复制代码
template<class F, class T>
T useF(F f, T x)
//"F f"可以接收函数指针、仿函数、lambda表达式
//"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;

	// lambda表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;


	//局部的静态变量(count)的地址是不同的三个,说明useF()被实例化成了不同的三份

	return 0;
}

useF()中的count是一个局部的静态变量,在同一份代码中理应只有一个固定的地址。"F f"接收了不同的类型,导致实例化出的每个useF()中的静态变量count,虽然拥有相同的值"1",但地址差别较大;且实例化出的每个useF(),处理"T x"接收到的相同的数据,得到的结果均不同。这说明因"F f"接收的类型不同,useF()被实例化成了三个不同的模板函数。而这样的模板实例化,每次都要生成一份专门处理相应类型的代码,当代码量很大时,实例化的代价也很大,效率很低。

当函数、仿函数、lambda表达式都是现成的时候,理想的情况应该是,useF()只被实例化出一份,传函数指针的时候可以去调用相应函数完成任务,传仿函数的时候可以去调用相应的仿函数完成任务,lambda表达式同理。这样,模板实例化的代价较小,效率也很高。

为了解决这个问题,C++11提供了function包装器。

2.function<( )> - 封装函数

function本质是一个可以封装函数的通用类模板,可以封装不同类型的可调用对象(如函数指针、仿函数、lambda 表达式等),以及它们的参数类型和返回类型。

cpp 复制代码
//std::function在头文件<functional>中,
//它的类模板原型如下:
template <class T> function;    
template <class Ret, class... Args>
class function<Ret(Args...)>;
//T:所调用函数的形参类型 
//Ret: 所调用函数的返回类型
//Args...:一个参数包,包含了所调用函数的形参

function包装器可以很好地解决上文引例中useF()被实例化出多份的问题:

cpp 复制代码
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()
{
	// 函数指针
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;

	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;

	// lamber表达式
	std::function<double(double)> func3 = [](double d)->double { return d / 4; };
	cout << useF(func3, 11.11) << endl;

	return 0;
}

以上代码中,"F f"接收的是function包装器。在useF()每次被调用的时候,静态变量count地址都相同,但每次调用后的值都不同。这说明useF()只被实例化了一份,传函数指针的包装器可以去调用相应的函数,传仿函数的包装器可以去调用相应的仿函数,传lambda表达式的包装器可以去调用相应的lambda表达式。

这就是function包装器的功能,它提供了一种统一的方式来处理这些可调用对象(函数指针、仿函数、lambda 表达式等),使它们的类型可以在代码运行后再去确定。

定义一个function包装器的语法格式是:

function<返回类型(形参类型...)> 变量名 = 函数名/仿函数类名/lambda表达式;

以下是关于function包装器的用法的更多说明:

cpp 复制代码
//1 一般方式

//普通函数
int f(int a, int b)
{
	return a + b;
}
//仿函数
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};
//Plus类
class Plus
{
public:
	//静态成员函数
	static int plusi(int a, int b)
	{
		return a + b;
	}

	//普通成员函数
	double plusd(double a, double b)
	{
		return a + b;
	}
};
int main()
{
	// 函数名(函数指针)
	std::function<int(int, int)> func1 = f; 
	cout << func1(1, 2) << endl;

	// 函数对象
	std::function<int(int, int)> func2 = Functor(); 
	cout << func2(1, 2) << endl;

	// lambda表达式
	std::function<int(int, int)> func3 = [](const int a, const int b)
		{return a + b; }; 
	cout << func3(1, 2) << endl;

	// 类的静态成员函数
	std::function<int(int, int)> func4 = &Plus::plusi; 
	cout << func4(1, 2) << endl;

	// 类的成员函数需要用对象调用
	std::function<double(Plus, double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(), 1.1, 2.2) << endl; 
	return 0;
}
cpp 复制代码
//2 与容器结合

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

int main()
{
	function<int(int, int)> f1 = Plus;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b)
		{
			cout << "[](int a, int b) {return a + b;}" << endl;
			return a + b;
		};

	cout << f1(1, 2) << endl;
	cout << f2(10, 20) << endl;
	cout << f3(100, 200) << endl << endl;

    

	// function又做map第二个模板参数,此时就可以借助map声明可调用对象类型
	// 且运行效果和上面代码是一样的
	map<string, function<int(int, int)>> opFuncMap;
	opFuncMap["函数指针"] = f;
	opFuncMap["仿函数"] = Functor();
	opFuncMap["lambda"] = [](int a, int b)
		{
			cout << "[](int a, int b) {return a + b;}" << endl;
			return a + b;
		};
	cout << opFuncMap["函数指针"](1, 2) << endl;
	cout << opFuncMap["仿函数"](10, 20) << endl;
	cout << opFuncMap["lambda"](100, 200) << endl;

	return 0;
}

算法题:逆波兰表达式求值

cpp 复制代码
//3 在算法题中

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<long long> st;
        map<string, function<long long(long long, long long)>> opFuncMap =
        {
            {"+", [](long long a, long long b) {return a + b;}},
            {"-", [](long long a, long long b) {return a - b;}},
            {"*", [](long long a, long long b) {return a * b;}},
            {"/", [](long long a, long long b) {return a / b;}},
        };
 
        for (auto& str : tokens)
        {
            if (opFuncMap.count(str)) // 操作符
            {
                int right = st.top();//右操作数先出栈
                st.pop();
                int left = st.top();
                st.pop();
                st.push(opFuncMap[str](left, right));
            }
            else // 操作数
            {
                st.push(stoll(str));
            }
        }
        return st.top();
    }
};

3.bind() - 调整参数顺序

bind()也是包装器,本质是一个函数模板,可以接收一个可调用对象,然后生成一个新的可调用对象来适应原对象的参数列表。

bind()可以把一个原本接收N个参数的函数,通过绑定一些参数,返回一个接收M个参数的新函数。同时,bind()还可以调整实参传递的顺序。

bind()的用法见以下代码:

cpp 复制代码
//全局域中的函数
int Sub(int a, int b)
{
	return a - b;
}
double Plus(int a, int b, double rate)
{
	return (a + b) * rate;
}
double PPlus(int a, double rate, int b)
{
	return  rate * (a + b);
}

//类域中的函数
class SubType
{
public:
    //静态成员函数
	static int sub(int a, int b)
	{
		return a - b;
	}
    //普通成员函数
	int ssub(int a, int b, int rate)
	{
		return (a - b) * rate;
	}
};

int main()
{
	//bind可以调整参数的顺序

	//一、
	// 想绑定全局域中的函数

	//1.
	function<int(int, int)> rSub1 = bind(Sub, placeholders::_1, placeholders::_2);
	cout << rSub1(10, 5) << endl; //5
    //placeholders(译为占位符)是一个命名空间
    //placeholders::_1代表传的第一个参数,placeholders::_2代表传的第二个参数
    //rSub1(10, 5),10传给placeholders::_1再传给Sub的"int a",
    //5传给placeholders::_2再传给Sub的"int b"

	function<int(int, int)> rSub2 = bind(Sub, placeholders::_2, placeholders::_1);
	cout << rSub2(10, 5) << endl; //-5
	//rSub2(10, 5),10还是传给placeholders::_1但再传给Sub的"int b",
	//5传给placeholders::_2再传给Sub的"int a"
	
	//2.
	//不想传Plus的第三个参数
	//可以指定第三个参数为固定参数
	function<double(int, int)> Plus1 = bind(Plus, placeholders::_1, placeholders::_2, 4.0);
	function<double(int, int)> Plus2 = bind(Plus, placeholders::_1, placeholders::_2, 4.2);
	function<double(int, int)> Plus3 = bind(Plus, placeholders::_1, placeholders::_2, 4.4);
	cout << Plus1(5, 3) << endl;//32
	cout << Plus2(5, 3) << endl;//33.6
	cout << Plus3(5, 3) << endl;//35.2

	//固定的参数不参与参数的排序
	//placeholders::_1一定获取传的第一个参数,placeholders::_2一定获取传的第二个参数
	function<double(int, int)> PPlus1 = bind(PPlus, placeholders::_1, 4.0, placeholders::_2);
	function<double(int, int)> PPlus2 = bind(PPlus, placeholders::_1, 4.2, placeholders::_2);
	cout << PPlus1(5, 3) << endl;//32
	cout << PPlus2(5, 3) << endl;//33.6


	//二、
	// 想绑定一个类的成员函数
	
	//1.
	//对于静态成员函数,必须指定其所在的类域
	//类名前加不加取地址操作符均可,但建议加上
	 //function<double(int, int)> Sub1 = bind(SubType::sub, placeholders::_1, placeholders::_2);
	function<double(int, int)> Sub1 = bind(&SubType::sub, placeholders::_1, placeholders::_2);
	cout << Sub1(1, 2) << endl;//-1

	//2.
	//对于非静态的普通成员函数ssub,它实际拥有四个参数(隐含的this指针)
	//所以需要创建一个类对象,传对象的地址给一个仿函数(看似是传给隐含的this指针,实际是传给bind底层的仿函数)
	SubType st;
	function<double(int, int)> Sub2 = bind(&SubType::ssub, &st, placeholders::_1, placeholders::_2, 3);
	cout << Sub2(1, 2) << endl;//-3
	//但直接传匿名对象也是可以的
	function<double(int, int)> Sub3 = bind(&SubType::ssub, SubType(), placeholders::_1, placeholders::_2, 3);
	cout << Sub3(1, 2) << endl;//-3
	//这是因为bind的底层与lambda类似,是仿函数
	//这个仿函数调取相应的函数,可以通过指针调用,也可以通过对象调用

	return 0;
}

十、智能指针

(详见本小节内容,请移步至:【C++】智能指针

十一、线程库

(详见本小节内容,请移步至:【C++】线程库-CSDN博客

相关推荐
娅娅梨1 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
兵哥工控5 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
汤米粥7 分钟前
小皮PHP连接数据库提示could not find driver
开发语言·php
冰淇淋烤布蕾9 分钟前
EasyExcel使用
java·开发语言·excel
我爱工作&工作love我13 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
拾荒的小海螺16 分钟前
JAVA:探索 EasyExcel 的技术指南
java·开发语言
马剑威(威哥爱编程)40 分钟前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
娃娃丢没有坏心思42 分钟前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h43 分钟前
探索 C++20:C++ 的新纪元
c++·c++20
lexusv8ls600h1 小时前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20