(学习总结19)C++11 列表初始化、右值引用、移动语义、引用折叠与完美转发


以下代码环境为 VS2022 C++。

一、列表初始化

C++98 传统的 {}

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

#include <iostream>
using namespace std;

struct one
{
	int a;
	int b;
};

int main()
{
	int arr1[] = { 1, 2, 3, 4, 5 };
	int arr2[10] = { 0 };
	one get1 = { 1, 2 };

	return 0;
}

C++11 中的 {}

C++11以后想统一初始化方式,试图实现一切对象皆可用 {} 初始化,{} 初始化也叫做列表初始化

内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,如果编译器有优化会变成直接构造。

{} 初始化的过程中,可以省略掉 “ = ”。

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

#include <iostream>
#include <vector>
using namespace std;

struct one
{
	int a;
	int b;
};

class two
{
	int a;
	int b;
	int c;

public:

	two(int ta, int tb, int tc)
		:a(ta)
		, b(tb)
		,c(tc)
	{
		;
	}

	two(int ta, int tb = 20)
		:a(ta)
		,b(tb)
	{
		;
	}
};

int main()
{
	// C++98
	int arr1[] = { 1, 2, 3, 4, 5 };
	int arr2[10] = { 0 };
	one get1 = { 1, 2 };

	// 注意 C++98 支持单参数时类型转换,也可以不用 {}
	two get5 = { 20 };
	two get4 = 20;
	string str = "hahaha";

	// C++11
	// 内置类型支持 {}
	int num = { 0 };

	// 自定义类型支持 {}
	// 理论上会走 有参构造 + 下面介绍的移动构造,
	// 如果编译器有优化会直接 有参构造
	two get2 = { 1, 2, 3 };

	// 用 const 引用来引用 有参构造 的临时对象
	const two& get3 = { 3, 2, 1 };

	// 只支持 {} 初始化时,"=" 才可以省略
	int num2{ 5 };
	two get6{ 3, 1, 2 };
	const two& get7{ 2, 1, 3 };

	// 没有 {} 初始化这样会报错
	//int num2 5;
	//two get8 3, 1, 2;


	vector<two> v;

	// 容器元素添加时,{} 比有名对象与匿名对象更好
	v.push_back(get6);			// 有名对象
	v.push_back(two(1, 2, 3));	// 匿名对象

	v.push_back({ 1, 2, 3 });	// {}

	return 0;
}

C++11 中的 std::initializer_list

上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个 vector 对象,如果想用 N 个值去构造初始化,那么我们得实现很多个构造函数才能支持,C++11 库中提出了一个 std::initializer_list 的类, auto il = { 10, 20, 30 },这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束。

容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的 { x1, x2, x3… } 进行初始化。STL 中的容器支持任意多个值构成的 { x1, x2, x3… } 进行初始化,就是通过 std::initializer_list 的构造函数支持的。

关于 std::initializer_list 详细讲解可参考这篇文章:(学习总结15)C++11小语法与拷贝问题

C++11 {} 列表初始化 与 std::initializer_list 区别

{} 列表初始化 与 std::initializer_list 同样是使用 {}, 那两者有什么区别呢?答案是 {} 括起来的内容被初始化的对象

  1. 对于 {} 列表初始化,{} 括起来的变量类型可能相同也可能不同,要求是初始化单个对象的

  2. 对于 std::initializer_list,{} 括起来的变量类型一定相同,要求是初始化容器的

#include <iostream>
#include <vector>
using namespace std;

class one
{
	int _a;
	int _b;

public:

	one(int a, int b)
		:_a(a)
		,_b(b)
	{
		;
	}
};

class two
{
	string _name;
	int _age;

public:

	two(const string& name, int age)
		:_name(name)
		,_age(age)
	{
		;
	}
};

int main()
{
	// {} 列表初始化 初始化单个对象,{} 内元素类型是否相同要根据其构造函数来确定
	one get1 = { 1, 2 };
	two get2 = { "zhangsan", 20 };

	// std::initializer_list 初始化容器,{} 内元素类型相同都为 int
	vector<int> arr1 = { 1, 2, 3, 4, 5 };

	// 两者结合,
	// 外面 {} 是 std::initializer_list 用于初始化容器,元素类型都为 two 或 one,
	// 里面 {} 是 列表初始化 用于初始化单个对象
	vector<two> arr2 = { { "zhangsan", 20 }, { "lisi", 21 }, { "wanger", 22 } };
	vector<one> arr3 = { { 1, 2 }, { 2, 3 }, { 3, 4 }, { 4, 5 }, { 5, 6 }, { 6, 7 } };

	// 二维 vector 中,
	// 最外层 {} 是 std::initializer_list 用于初始化外层 vector 容器,元素类型都为 vector<two>,
	// 第二层 {} 是 std::initializer_list 用于初始化内层 vector 容器,元素类型都为 two,
	// 第三层 {} 是 列表初始化 用于初始化单个对象
	vector<vector<two>> arr4 = { { { "zhangsan", 20 }, { "lisi", 21 } }, { { "wanger", 22 }, { "mazi", 23 } } };

	return 0;
}

二、右值引用

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

左值和右值

左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给它赋值,但是可以取它的地址。

右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址

值得一提的是,左值的英文简写为 lvalue,右值的英文简写为 rvalue。传统认为它们分别是left value、right value 的缩写。现代 C++ 中,lvalue 被解释为 loacte value 的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别在于能否取地址

#include <iostream>
using namespace std;

int main()
{
	// 左值可以取地址
	// 以下是一些常见的左值
	int* p = new int(10);
	int a = 5;
	const int b = 20;
	string c = "haha";

	cout << &a << endl;
	cout << &b << endl;
	cout << (void*)&c[0] << endl;
	cout << &p << endl;

	// 右值不能取地址
	// 以下是一些常见的右值
	10;
	a + b;
	fmin(5.1, 6.5);
	string("hehe");

	//cout << &10 << endl;
	//cout << &(a + b) << endl;
	//cout << &fmin(5.1, 6.5) << endl;
	//cout << &string("hehe") << endl;

	return 0;
}

左值引用和右值引用

  1. 左值引用就是给左值取别名,同样的道理,右值引用就是给右值取别名

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

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

#include <iostream>
using namespace std;

int main()
{
	int a = 10;
	int& r1 = a;				// 左值引用
	//int& r2 = 10;				// 左值引用不能直接引用右值
	const int& r3 = 10;			// 但是 const 左值引用可以引用右值

	int&& rr1 = 10;				// 右值引用
	//int&& rr2 = a;			// 右值引用不能直接引用左值
	int&& rr3 = move(a);		// 但是右值引用可以引用 move(左值)

	return 0;
}

move 是库里面的一个函数模板,本质是进行强制类型转换,它还涉及一些引用折叠的知识。

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

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

#include <iostream>
using namespace std;

int main()
{
	// 左值可以取地址
	// 以下是一些常见的左值
	int* p = new int(10);
	int a = 5;
	const int b = 20;
	string c = "haha";

	int&		r1 = a;
	const int&	r2 = b;
	string&		r3 = c;
	int*&		r4 = p;

	// 右值不能取地址
	// 以下是一些常见的右值
	10;
	a + b;
	fmin(5.1, 6.5);
	string("hehe");

	int&&		rr1 = 10;
	int&&		rr2 = a + b;
	double&&	rr3 = fmin(5.1, 6.5);
	string&&	rr4 = string("hehe");
	string&&	rr5 = (string&&)c;

	// 右值引用变量其变量表达式的属性是左值,则可以取地址
	cout << &rr1 << endl;
	cout << &rr2 << endl;
	cout << &rr3 << endl;
	cout << &rr4 << endl;

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

	return 0;
}

引用延长生命周期

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

#include <iostream>
using namespace std;

int main()
{
	string str1 = "hello";
	string str2 = "world";

	//string& r1 = str1 + " " + str2;			// 左值引用不能引用右值

	const string& r2	= str1 + " " + str2;	// const 的左值引用延长临时对象生命周期
	//r2 += "!";								// 但对象无法被修改

	string&& rr1		= str1 + " " + str2;	// 右值引用延长临时对象生命周期
	rr1 += "!";									// 对象可以被修改

	const string&& rr2	 = str1 + " " + str2;	// const 的右值引用延长临时对象生命周期
	//rr2 += "!";								// 但对象无法被修改

	cout << r2 << endl;
	cout << rr1 << endl;
	cout << rr2 << endl;

	return 0;
}

左值和右值的参数匹配

C++98 中,实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。

C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 func 函数,那么实参是 左值 会匹配 func(左值引用),实参是 const 左值 会匹配 func(const 左值引用),实参是 右值 会匹配 func(右值引用)。

右值引用变量在用于表达式时属性是左值,主要是为了能够转移右值里的资源(使用 const 左值引用时引用右值是不能修改右值的),实现移动语义,下面我们讲到移动语义的移动构造与移动赋值场景时,就能体会这样设计的价值。

#include <iostream>
using namespace std;

void func(int& num)
{
	cout << "调用左值引用函数 func(" << num << ")" << endl;
}

void func(const int& num)
{
	cout << "调用 const 左值引用函数 func(" << num << ")" << endl;
}

void func(int&& num)
{
	cout << "调用右值引用函数 func(" << num << ")" << endl;
}

// 当没有 右值引用参数的func,
// 但有 const 右值引用参数的func,
// 会自动匹配 const 右值引用func
void func(const int&& num)
{
	cout << "调用 const 右值引用函数 func(" << num << ")" << endl;
}

int main()
{
	int a = 10;
	const int b = 20;
	func(a);
	func(b);
	func(30);
	func(move(a));
	func(move(b));	// 注意 move 强制转化时不会将变量的 const 属性丢失

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

	// 右值引用变量的属性是左值
	int&& rr1 = 40;
	func(rr1);
	func(move(rr1));

	return 0;
}

类型分类

C++11 以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称 prvalue)和将亡值 (expiring value,简称 xvalue)。

纯右值是指那些 字面值常量 或 求值结果相当于字面值 或是 一个无名的临时对象。纯右值和将亡值在 C++11 中提出,C++11 中的纯右值概念划分等价于 C++98 中的右值。

将亡值是指 返回右值引用的函数的调用表达式 和 转换为右值引用的转换函数的调用表达,如 move(左值)、static_cast<X&&>(x)

泛左值(generalized value,简称 glvalue),泛左值包含将亡值和左值。

值类别 - cppreference.comValue categories 这两个关于值类型的中文和英文的官方文档,有兴趣可以了解细节。

变量有名字,就是 glvalue;变量有名字,且不能被 move,就是 lvalue;变量有名字,且可以被 move,就是 xvalu;变量没有名字,且可以被移动,则是 prvalue。
在这里插入图片描述

三、移动语义

在 C++11 之前,对象的赋值和传递通常是通过复制来完成的。但是在很多情况下,这种复制操作是不必要的,特别是当对象内部包含一些资源,复制这些资源会带来性能开销。

移动语义允许将资源从一个对象转移到另一个对象,而不是进行代价更大的复制操作。这就好比你要搬家,以前是把所有东西重新买新的(复制)一份放到新家,现在是直接把东西从旧家搬(转移)到新家,这样可以减少浪费,大大提高效率。

移动构造和移动赋值

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

  2. 移动赋值是一个赋值运算符的重载,它跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用

#include <iostream>
using namespace std;

class a
{
private:

	int* _p = nullptr;

public:

	a(int num = 0)
		:_p(new int(num))
	{
		;
	}

	~a()
	{
		if (_p != nullptr)
		{
			delete _p;
			_p = nullptr;
		}
	}

	a(const a& one)
	{
		cout << "调用拷贝构造 a(const a& one)" << endl;
		_p = new int(*(one._p));
	}

	a& operator=(const a& one)
	{
		cout << "调用拷贝赋值 a& operator=(const a& one)" << endl;
		if (&one != this)
		{
			_p = new int(*(one._p));
		}
		return *this;
	}

	void swap(a& two)
	{
		std::swap(_p, two._p);
	}

	a(a&& one)				// 我实现的是交换的方式,只要符合要求即可
	{
		cout << "调用移动构造 a(a&& one)" << endl;
		swap(one);	// 将资源进行交换
	}

	a& operator=(a&& one)
	{
		cout << "调用移动赋值 a& operator=(a&& one)" << endl;
		swap(one);	// 将资源进行交换
		return *this;
	}
};

int main()
{
	a t1 = 1;				// 有参构造

	a t2 = t1;				// 拷贝构造,拷贝出新资源

	a t3 = move(t2);		// 移动构造,两者资源交换

	t2 = t1;				// 拷贝赋值,拷贝出新资源

	t3 = move(t2);			// 移动赋值,两者资源交换
	
	a t4 = a(5);			// 有参构造 + 移动构造,这里 VS2022 优化成只剩 有参构造

	return 0;
}

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

右值引用与移动语义的关系

右值是一个表达式,它要么是一个临时对象(如函数返回值),要么是一个即将销毁的对象(例如通过 std::move 转换后的对象)。

在没有移动语义的情况下,临时对象或即将销毁的对象 会被复制到调用函数的地方。但有了移动语义和右值引用,调用移动构造函数来直接将资源从它们内部转移到目标对象,而不是重新复制一份资源。

函数重载时参数的右值引用可以匹配临时对象和即将销毁的对象,则可以这样联系两者关系:

  1. 对于临时对象,右值引用的匹配是自动的,在有移动语义的情况下(有移动构造与移动赋值)会自动将资源进行转移,无形中减少许多不必要的拷贝。

  2. 对于即将销毁的对象,右值引用的匹配是手动的(例如需要程序员自己用 std::move 标明),在有移动语义且标明的情况下会将资源进行转移,明确的减少拷贝。

  3. 则右值引用负责匹配,移动语义负责减少其拷贝。

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

下面是一个简单的大数加法运算,理论上我们可以推测出资源转移的次数,但是考虑到使用的是 VS2022,其进行了很多优化,可能不会与理论上的相同:

#include <iostream>
#include <string>
using namespace std;

string add(const string& one, const string& two)
{
	int len1 = one.size() - 1;
	int len2 = two.size() - 1;
	int next = 0;							// 高位存储

	string copy;
	copy.reserve((len1 > len2 ? len1 : len2) + 5);

	while (len1 >= 0 || len2 >= 0 || next > 0)
	{
		int num1 = len1 >= 0 ? one[len1--] - '0' : 0;
		int num2 = len2 >= 0 ? two[len2--] - '0' : 0;
		int ret = num1 + num2 + next;		// 当前位数计算

		next = ret / 10;					// 进高位
		ret = ret % 10;						// 保留低位

		copy += (ret + '0');				// 存进结果
	}

	reverse(copy.begin(), copy.end());		// 翻转
	return move(copy);						// 第一次转移资源,将 copy 里的资源转移给临时变量
}

int main()
{
	string ret = add("1234", "5678");		// 第二次转移资源,将临时变量的资源转移给 ret
	cout << ret << endl;					// 可以推测 add 里的局部变量 copy 的资源是进行 两次转移 而不是 两次拷贝,提高了效率

	return 0;
}

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

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

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

这里以 vector 的 push_back 举例:
在这里插入图片描述

#include <iostream>
#include <vector>
#include <string>
using namespace std;

int main()
{
	vector<string> arr;

	string str1 = "1111111111111111";

	arr.push_back(str1);						// 拷贝构造

	arr.push_back(string("2222222222222222"));	// 有参构造 + 移动构造

	arr.push_back("3333333333333333");			// 有参构造 + 移动构造

	arr.push_back(move(str1));					// 移动构造

	for (auto& e : arr)
	{
		cout << e << endl;
	}

	return 0;
}

四、引用折叠

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

#include <iostream>
using namespace std;

typedef int& lref;		// 左值引用
typedef int&& rref;		// 右值引用

int main()
{
	int num = 0;
	lref&	r1 = num;	// 左值引用 + 左值引用 -> 左值引用
	lref&&	r2 = num;	// 左值引用 + 右值引用 -> 左值引用
	rref&	r3 = num;	// 右值引用 + 左值引用 -> 左值引用
	rref&&	rr1 = 10;	// 右值引用 + 右值引用 -> 右值引用

	return 0;
}

万能引用

像 func2 这样的函数模板中,T&& x 参数看起来是右值引用参数,但是由于引用折叠的规则,它传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用

#include <iostream>
using namespace std;

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

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

int main()
{
	int num = 0;

	// 自动类型推导
	// func1 可以推导左值,不能推导右值
	func1(num);
	//func1(0);				// 报错

	// func2 不仅可以推导左值,也可以推导右值
	func2(num);
	func2(0);

	// 这里显示实例化是方便大家探究

	// func1 
	// 没有折叠->实例化为 void func1(int& x);
	func1<int>(num);
	//func1<int>(0);		// 报错

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

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

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

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

	// func2
	// 没有折叠->实例化为 void func2(int&& x);
	//func2<int>(num);			// 报错
	func2<int>(0);		

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

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

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

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

	return 0;
}

万能引用推导细节

Function(T&& x) 函数模板程序中,假设实参是 int 右值,则模板参数 T 推导结果为 int,实现了实参可以是右值。实参是 int 左值,模板参数 T 的推导结果为 int&,再结合引用折叠规则,就实现了实参可以是左值。

则从整体上看,实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引用版本形参的 Function,但是其中还有很多细节:

#include <iostream>
using namespace std;

template<class T>
void Function(T&& x)
{
	int one = x;
	T derivation = one;
	//derivation++;

	cout << &one << endl;
	cout << &derivation << endl;
}

int main()
{
	// 注意这里万能引用自动推导右值时会将 T 推导为 int 而不是 int&&,
	// 前者会直接与 && 匹配,而后者会触发引用折叠
	// 结果模版实例化为 void Function(int&& x);
	// 由于 T 推导为 int,derivation 则是一个 int类型变量
	// 所以 one 与 derivation 的地址不一样
	Function(0);

	// T 推导为 int&,引用折叠,模版实例化为 void Function(int& x);
	// derivation 类型为 int&,则它与 one 的地址一样
	int a = 10;
	Function(a);

	// T 推导为 int,模版实例化为 void Function(int&& x);
	// derivation 类型为 int,则它与 one 的地址不一样
	Function(move(a));

	// T 推导为 const int&,引用折叠,模版实例化为 void Function(const int& x);
	// derivation 类型为 const int&,它与 one 地址一样,但是不能 ++ 修改
	const int b = 20;
	Function(b);

	// T 推导为 const int,模版实例化为 void Function(const int&& x);
	// derivation 类型为 const int,它与 one 地址不一样,不能 ++ 修改
	Function(move(b));

	return 0;
}

五、完美转发

Function(T&& x)函数模板程序中,传左值实例化以后是左值引用的 Function 函数,传右值实例化以后是右值引用的 Function 函数。

但是结合我们在前面的讲解,左值引用变量 与 右值引用变量 表达式都是左值属性,这意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,则 Function 函数中 x 的属性是左值,那么我们把 x 传递给下一层函数 func1,匹配的都是左值引用版本的 func1 函数。如果想要保持 x 对象的属性,就需要使用完美转发实现。

完美转发 forward 本质是一个函数模板,它主要还是通过引用折叠的方式实现,下面示例中传递给 Function 的实参是右值,T 被推导为 int,没有折叠,forward 内部 x 被强转为右值引用返回;传递给 Function 的实参是左值,T 被推导为 int&,引用折叠为左值引用,forward 内部 x 被强转为左值引用返回。

#include <iostream>
using namespace std;

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

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

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

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

template<class T>
void Function(T&& x)
{
	// 我们知道,右值引用变量自身属性也是左值
	// 没有完美转发,func1 这里的 x 一直是一个左值
	//func1(x);
	
	// 这里的 T 可以不加上 &&,
	// 在之前的程序中已经知道 T 只有两种推导:int 或 int&
	// 而 int 在 forward 也为 右值, int& 为左值
	func1(forward<T>(x));
	//func1(forward<T&&>(x));
}

int main()
{
	// 有完美转发会调用 右值的 func1,
	// 没有就调用 左值的 func1。
	Function(0);
	
	// 调用 左值的 func1
	int a = 10;
	Function(a);
	
	// 有完美转发调用 右值的 func1,
	// 没有调用 左值的 func1
	Function(move(a));
	
	// 调用 const 左值的 func1
	const int b = 20;
	Function(b);
	
	// 有完美转发调用 const 右值的 func1
	// 没有调用 const 左值的 func1
	Function(move(b));

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值