C++11(列表初始化、右值引用和移动语义)

C++11(列表初始化、右值引用和移动语义)

  • [1. 列表初始化](#1. 列表初始化)
    • [1.1 C++98传统的{}](#1.1 C++98传统的{})
    • [1.2 C++11中的{}](#1.2 C++11中的{})
    • [1.3 C++11中的std::initializer_list](#1.3 C++11中的std::initializer_list)
  • [2. 右值引用和移动语义](#2. 右值引用和移动语义)
    • [2.1 左值和右值](#2.1 左值和右值)
    • [2.2 左值引用和右值引用](#2.2 左值引用和右值引用)
    • [2.3 引用延长生命周期](#2.3 引用延长生命周期)
    • [2.4 左值和右值的参数匹配](#2.4 左值和右值的参数匹配)
    • [2.5 右值引用和移动语义的使用场景](#2.5 右值引用和移动语义的使用场景)
      • [2.5.1 左值引用主要使用场景回顾](#2.5.1 左值引用主要使用场景回顾)
      • [2.5.2 移动构造和移动赋值](#2.5.2 移动构造和移动赋值)
      • [2.5.3 右值引用和移动语义解决船只返回问题](#2.5.3 右值引用和移动语义解决船只返回问题)
      • [2.5.4 右值引用和移动语义在容器插入中的提效](#2.5.4 右值引用和移动语义在容器插入中的提效)
    • [2.6 类型分类](#2.6 类型分类)
    • [2.7 引用折叠](#2.7 引用折叠)
    • [2.8 完美转发](#2.8 完美转发)
      • [2.8.1 完美转发场景演示](#2.8.1 完美转发场景演示)

1. 列表初始化

1.1 C++98传统的{}

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

1.2 C++11中的{}

  1. C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
  2. 内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,优化了之后变成直接构造。
  3. {}初始化的过程中,可以省略掉 =
  4. C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push / insert多参数构造的对象时,{}初始化会很⽅便
cpp 复制代码
#include<iostream>
#include<vector>

using namespace std;

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 array1[] = { 1, 2,  3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };

	// C++11支持的
	// 内置类型支持
	int x1 = { 2 };
	// 自定义类型支持
	// 这里本质是用{ 2025, 1, 1 }构造一个Date临时对象
	// 临时对象再去拷贝构造给d1,编译器优化后合二为一变成{ 2025, 1, 1 }直接构造初始化

	// 运行一下,可以验证上面的理论,发现是没调用拷贝构造的
	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 d5{ 2024, 7, 25 };
	const Date& d6{ 2024, 7, 25 };

	// 不支持Date d7 2025; 只有{}初始化才能省略 =

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

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

	return 0;
}

1.3 C++11中的std::initializer_list

  1. 上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector< int > v1 ={1,2,3};vector< int > v2 = {1,2,3,4,5};
  2. C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷⻉过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
  3. 这是他的⽂档:initializer_list,std::initializer_list⽀持迭代器遍历。
  4. 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过std::initializer_list的构造函数⽀持的。
cpp 复制代码
// STL中的容器都增加了⼀个initializer_list的构造
vector (initializer_list<value_type> il, const allocator_type& alloc =
allocator_type());
list (initializer_list<value_type> il, const allocator_type& alloc =
allocator_type());
map (initializer_list<value_type> il,const key_compare& comp =
key_compare(),const allocator_type& alloc = allocator_type());
// ...

template<class T>
class vector 
{
public:
	typedef T* iterator;
	vector(initializer_list<T> l)
	{
		for (auto e : l)
			push_back(e)
	}
private:
	iterator _start = nullptr;
	iterator _finish = nullptr;
	iterator _endofstorage = nullptr;
};

// 另外,容器的赋值也⽀持initializer_list的版本
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);
cpp 复制代码
#include<iostream>
#include<vector>
#include<string>
#include<map>

using namespace std;

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

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

	// {}列表中可以有任意多个值
	// 这两个写法语义上还是有差别的,第一个v1是直接构造
	// 第二个v2是构造临时对象+临时对象拷贝给v2->优化为直接构造
	vector<int> v1({ 1, 2, 3, 4, 5 });
	vector<int> v2 = { 1, 2, 3, 4, 5 };
	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;
}

打印16是因为在64位环境下指针变量占8个字节空间,两个就是16

2. 右值引用和移动语义

C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。

2.1 左值和右值

  1. 左值是⼀个表示数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现在赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
  2. 右值也是⼀个表示数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
  3. 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
cpp 复制代码
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("11111");
	s[0] = 'x';

	cout << &c << endl;
	cout << (void*)&s[0] << endl;

	// 右值:不能取地址
	double x = 1.1, y = 2.2;
	// 以下的10、x + y、fmin(x, y)、string("11111")都是常见的右值
	10;
	x + y;
	fmin(x, y);
	string("11111");

	cout << &10 << endl;
	cout << &(x+y) << endl;
	cout << &(fmin(x, y)) << endl;
	cout << &string("11111") << endl;

	return 0;
}

2.2 左值引用和右值引用

  1. Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
  2. 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
  3. 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
  4. template < class T > typename remove_reference< T >::type&& move (T&& arg);
  5. move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知识,这个我们后⾯会细讲。
  6. 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
  7. 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编的实现和上层语法表达的意义有时是背离的,所以不要燃到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。

6、7现在不理解很正常,等2.5.4讲解完后再来看6、7就理解了。

cpp 复制代码
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{ 	// forward _Arg as movable
	return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
 
#include<iostream>

using namespace std;

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("11111");
	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左值引用可以引用右值
	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);
	//int&& rrx3 = (int&&)(*p);
	string&& rrx4 = move(s);
	string&& rrx5 = (string&&)s;

	// b、r1、rr1都是变量表达式,都是左值属性
	cout << &b << endl;
	cout << &r1 << endl;
	cout << &rr1 << endl;

	// 这里要注意的是,rr1的属性是左值,所以不能再被右值引用绑定,除非move一下
	int& r6 = r1;
	// int&& rrx6 = rr1; // err
	int&& rrx6 = move(rr1);

	return 0;
}

2.3 引用延长生命周期

右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。

cpp 复制代码
int main()
{
	string s1 = "Test";
	//string&& r1 = s1; // err:不能绑定到左值

	const string& r2 = s1 + s1; // ok:const 的左值引用延长生命周期
	//r2 += "Test"; // err:不能对 const 的引用修改

	string&& r3 = s1 + s1; // ok:右值引用延长生命周期
	r3 += "Test"; // ok:可以对非 const 的引用修改

	cout << r3 << endl;

	return 0;
}

2.4 左值和右值的参数匹配

  1. C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
  2. C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
  3. 右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,2.5.4我们讲右值引⽤的使⽤场景时,就能体会这样设计的价值了
cpp 复制代码
#include<iostream>

using namespace std;

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

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

void f(int&& x)
{
	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(move(i)); // 调用 f(int&&)

	// 右值引用变量在用于表达式时是左值属性
	int&& x = 1;
	f(x); // 调用 f(int&)
	f(move(x));// 调用 f(int&&)

	return 0;
}

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

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

左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法改变对象已经析构销毁的事实。

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

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

2.5.2 移动构造和移动赋值

  1. 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
  2. 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
  3. 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要"窃取"引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的bs::string样例实现了移动构造和移动赋值,我们需要结合场景理解。
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cassert>
#include<cstring>
#include<algorithm>

using namespace std;

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

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end() const
		{
			return _str + _size;
		}

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

		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)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
			
			if (this != &s)
			{
				_str[0] = '\0';
				_size = 0;

				reserve(s._capacity);
				for (auto ch : s)
				{
					push_back(ch);
				}
			}

			return *this;
		}

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

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

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

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

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

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

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

		size_t size() const
		{
			return _size;
		}

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

int main()
{
    bs::string s1("xxxxx");
    // 拷贝构造
	bs::string s2 = s1;
    // 构造 + 移动构造,优化后直接构造
    bs::string s3 = bs::string("yyyyy");
    // 移动构造
    bs::string s4 = move(s1);
    cout << "******************************" << endl;

    return 0;
}

他的本质是要"窃取"引⽤的右值对象的资源

cpp 复制代码
// 移动构造
string(string&& s)
{
	cout << "string(string&& s) -- 移动构造" << endl;
	swap(s);
}

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

2.5.3 右值引用和移动语义解决船只返回问题

cpp 复制代码
namespace bs
{
	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()
{
	bs::string ret = bs::addStrings("11111", "22222");
	cout << ret.c_str() << endl;

	return 0;
}

// 场景2
int main()
{
	bs::string ret;
	ret	= bs::addStrings("11111", "22222");
	cout << ret.c_str() << endl;

	return 0;
}

右值对象构造,只有拷⻉构造,没有移动构造的场景

  1. 下图展示了编译器不优化的情况下,两次拷⻉构造,vs2019 debug环境下编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造,vs2019的release和vs2022的debug和release环境下编译器直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造,要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解。
  2. linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide-constructors 的⽅式关闭构造优化,运⾏结果可以看到没有优化的两次拷⻉。

无优化

vs2019 debug 优化

vs2019的release和vs2022的debug和release 优化

右值对象构造,有拷⻉构造,也有移动构造的场景

  1. 下图展示了编译器不优化的情况下,两次移动构造,vs2019 debug环境下编译器优化的场景下连续步骤中的移动合⼆为⼀变为⼀次移动构造,vs2019的release和vs2022的debug和release环境下编译器直接将str对象的构造,str移动构造临时对象,临时对象移动构造ret对象,合三为⼀,变为直接构造,要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解。
  2. linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动。

无优化

vs2019 debug 优化

vs2019的release和vs2022的debug和release 优化

这两个图跟拷贝构造的类似,不再展示。

右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景

  1. 下图展示了 g++ test.cpp -fno-elide-constructors 关闭优化环境和vs2019 debug下编译器的处理。
  2. 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造。


右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景

  1. 下图展示了 g++ test.cpp -fno-elide-constructors 关闭优化环境和vs2019 debug下编译器的处理。
  2. 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造。


2.5.4 右值引用和移动语义在容器插入中的提效

  1. 查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝d都增加了右值引⽤版本
  2. 当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象
  3. 当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源转移到容器空间的对象上
  4. 把我们之前模拟实现的bs::list拷⻉过来,⽀持右值引⽤参数版本的push_back和insert
  5. 其实这⾥还有⼀个emplace系列的接⼝,但是这个涉及可变参数模板,我们需要把可变参数模板讲解以后再讲解emplace系列的接⼝。

先看STL库里的效果

cpp 复制代码
int main()
{
	std::list<bs::string> lt;
	bs::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;
	lt.push_back(bs::string("22222222222222222222222222222"));
	cout << "*************************" << endl;
	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;
	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}
cpp 复制代码
//list.h
// 无关接口删除了
namespace bs
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next = nullptr;
		list_node<T>* _prev = nullptr;

		list_node(const T& val)
			:_data(val)
			,_next(nullptr)
			,_prev(nullptr)
		{}
		
		// insert中的val传到这
		list_node(T&& val = T())
			// 再把val强转成右值属性
			// val会去调用bs:string的移动构造
			:_data(move(val))
			, _next(nullptr)
			, _prev(nullptr)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		Node* _node;

		__list_iterator(Node* node)
			:_node(node)
		{}

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

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

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

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<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& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);

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

			++_size;

			//返回新插入节点位置的迭代器
			return iterator(newnode);
		}

		// 假设val具有右值属性,那val应该具有常性,不能改变
		// 但是移动构造/移动赋值需要改变它,所以val必须具有左值属性
		iterator insert(iterator pos, T&& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			// 既然val具有左值属性,那么要想调用移动构造
			// 要把val强转成右值属性传过去才行
			Node* newnode = new Node(move(val));

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

			++_size;

			//返回新插入节点位置的迭代器
			return iterator(newnode);
		}

	private:
		Node* _head;
		size_t _size = 0;
	};
}
cpp 复制代码
#include "list.h"

int main()
{
	bs::list<bs::string> lt;
	cout << "*************************" << endl;
	bs::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;
	lt.push_back(bs::string("22222222222222222222222222222"));
	cout << "*************************" << endl;
	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;
	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}

可以看到,我们调用的的跟库里面是一样的,只不过我们的list.h加了一个哨兵位,所以会为哨兵位准备一个节点。

2.6 类型分类

  1. C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
  2. 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调⽤,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于
    C++98中的右值。
  3. 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如move(x)、static_cast<X&&>(x)
  4. 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
  5. 值类别 - cppreference.comValue categories这两个关于值类型的中⽂和英⽂的官⽅⽂档,有兴趣可以了解细节。

2.7 引用折叠

  1. C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef 中的类型操作可以构成引⽤的引⽤。
  2. 通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
  3. 下⾯的程序中很好的展示了模板和typedef构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀个仔细理解⼀下。
  4. 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤。
  5. Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导是int,实参是int左值,模板参数T的推导是int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function。
cpp 复制代码
typedef int& lref;
typedef int&& rref;

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

// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{
	int n = 0;
	lref& r1 = n; // r1 的类型是 int&
	lref&& r2 = n; // r2 的类型是 int&
	rref& r3 = n; // r3 的类型是 int&
	rref&& r4 = 1; // r4 的类型是 int&&

	// 没有折叠->实例化为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);

	// 没有折叠->实例化为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;
}
cpp 复制代码
// 引用折叠 -> 万能引用
// 传左值,就实例化出左值引用的函数
// 传右值,就实例化出右值引用的函数
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;
}

左值引用地址相同,右值引用地址不同

2.8 完美转发

  1. Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。
  2. 但是结合我们之前说变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。
  3. template < class T > T&& forward (typename remove_reference< T >::type& arg);
  4. template < class T > T&& forward (typename remove_reference< T >::type&& arg);
  5. 完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯示例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回。
cpp 复制代码
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
	return static_cast<_Ty&&>(_Arg);
}

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(forward<T>(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;
}

2.8.1 完美转发场景演示

用我们写的list.h,将它的push_back和insert改为万能引用的版本,就不需要写一个左值引用版本和一个右值引用版本了。

cpp 复制代码
#pragma once

//list.h
// 无关接口删除了
namespace bs
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next = nullptr;
		list_node<T>* _prev = nullptr;

		//list_node(const T& val)
		//	:_data(val)
		//	, _next(nullptr)
		//	, _prev(nullptr)
		//{}

		//// insert中的val传到这
		//list_node(T&& val = T())
		//	// 再把val强转成右值属性
		//	// val会去调用bs:string的移动构造
		//	:_data(move(val))
		//	, _next(nullptr)
		//	, _prev(nullptr)
		//{}

		list_node() = default;

		// 这里写万能引用效果不好
		// 得加一个默认构造,因为我们在new Node时,
		// 函数模板的X是泛型,还未实例化
		template<class X>
		list_node(X&& val)
			:_data(forward<X>(val))
			, _next(nullptr)
			, _prev(nullptr)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> Self;
		Node* _node;

		__list_iterator(Node* node)
			:_node(node)
		{
		}

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

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

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

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<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));
		//}

		// 万能引用
		template<class X>
		void push_back(X&& x)
		{
			insert(end(), forward<X>(x));
		}

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

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

		//	++_size;

		//	//返回新插入节点位置的迭代器
		//	return iterator(newnode);
		//}

		//// 假设val具有右值属性,那val应该具有常性,不能改变
		//// 但是移动构造/移动赋值需要改变它,所以val必须具有左值属性
		//iterator insert(iterator pos, T&& val)
		//{
		//	Node* cur = pos._node;
		//	Node* prev = cur->_prev;
		//	// 既然val具有左值属性,那么要想调用移动构造
		//	// 要把val强转成右值属性传过去才行
		//	Node* newnode = new Node(move(val));

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

		//	++_size;

		//	//返回新插入节点位置的迭代器
		//	return iterator(newnode);
		//}
		
		// X的类型是实参传递给形参推出来的
		// 万能引用
		template<class X>
		iterator insert(iterator pos, X&& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(forward<X>(val));

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

			++_size;

			//返回新插入节点位置的迭代器
			return iterator(newnode);
		}

	private:
		Node* _head;
		size_t _size = 0;
	};
}
cpp 复制代码
#include "list.h"

int main()
{
	bs::list<bs::string> lt;
	cout << "*************************" << endl;
	bs::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;
	lt.push_back(bs::string("22222222222222222222222222222"));
	cout << "*************************" << endl;
	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;
	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}
相关推荐
勇闯逆流河3 小时前
【C++】红黑树详解
开发语言·数据结构·c++
小杰帅气5 小时前
类与对象1
开发语言·c++
chenyuhao20245 小时前
《C++二叉引擎:STL风格搜索树实现与算法优化》
开发语言·数据结构·c++·后端·算法
空荡forevere6 小时前
《操作系统真象还原》 第十章 输入输出系统
开发语言·c++·操作系统
沢田纲吉7 小时前
《LLVM IR 学习手记(五):关系运算与循环语句的实现与解析》
前端·c++·llvm
沢田纲吉7 小时前
《LLVM IR 学习手记(六):break 语句与 continue 语句的实现与解析》
前端·c++·llvm
爱和冰阔落7 小时前
【C++进阶】继承上 概念及其定义 赋值兼容转换 子类默认成员函数的详解分析
c++
余辉zmh8 小时前
【C++篇】:LogStorm——基于多设计模式下的同步&异步高性能日志库项目
开发语言·c++·设计模式
艾莉丝努力练剑8 小时前
【C++STL :list类 (二) 】list vs vector:终极对决与迭代器深度解析 && 揭秘list迭代器的陷阱与精髓
linux·开发语言·数据结构·c++·list