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

news2025/1/16 4:05:45

C++11 列表初始化、右值引用、移动语义、引用折叠与完美转发

  • 一、列表初始化
    • C++98 传统的 {}
    • C++11 中的 {}
    • C++11 中的 std::initializer_list
    • C++11 {} 列表初始化 与 std::initializer_list 区别
  • 二、右值引用
    • 左值和右值
    • 左值引用和右值引用
    • 引用延长生命周期
    • 左值和右值的参数匹配
    • 类型分类
  • 三、移动语义
    • 移动构造和移动赋值
    • 右值引用与移动语义的关系
    • 右值引用和移动语义解决传值返回问题
    • 右值引用和移动语义在传参中的提效
  • 四、引用折叠
    • 万能引用
    • 万能引用推导细节
  • 五、完美转发

以下代码环境为 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.com 和 Value 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;
}

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

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

相关文章

从0开始学习搭网站第二天

前言&#xff1a;今天比较惭愧&#xff0c;中午打铲吃了一把&#xff0c;看着也到钻二了&#xff0c;干脆顺手把这个赛季的大师上了&#xff0c;于是乎一直到网上才开始工作&#xff0c;同样&#xff0c;今天的学习内容大多来自mdn社区mdn 目录 怎么把文件上传到web服务器采用S…

ffmpeg硬件编码

使用FFmpeg进行硬件编码可以显著提高视频编码的性能&#xff0c;尤其是在处理高分辨率视频时。硬件编码利用GPU或其他专用硬件&#xff08;如Intel QSV、NVIDIA NVENC、AMD AMF等&#xff09;来加速编码过程。以下是使用FFmpeg进行硬件编码的详细说明和示例代码。 1. 硬件编码支…

【高可用自动化体系】自动化体系

架构设计的愿景就是高可用、高性能、高扩展、高效率。为了实现架构设计四高愿景&#xff0c;需要实现自动化系统目标&#xff1a; 标准化。 流程自助化。 可视化&#xff1a;可观测系统各项指标、包括全链路跟踪。 自动化&#xff1a;ci/cd 自动化部署。 精细化&#xff1a…

elasticsearch中IK分词器

1、什么是IK分词器 ElasticSearch 几种常用分词器如下&#xff1a; 分词器分词方式StandardAnalyzer单字分词CJKAnalyzer二分法IKAnalyzer词库分词 分词∶即把一段中文或者别的划分成一个个的关键字&#xff0c;我们在搜索时候会把自己的信息进行分词&#xff0c;会把数据库…

arcgis中生成格网矢量带高度

效果 1、数据准备 (1)矢量边界(miain.shp) (2)DEM(用于提取格网标高) (3)DSM(用于提取格网最高点) 2、根据矢量范围生成格网 模板范围选择矢量边界,像元宽度和高度根据坐标系来输入,我这边是4326的,所以输入的是弧度,输出格网矢量gewang.shp 3、分区统计 …

一文了解如何使用 DBeaver 管理 DolphinDB

在日常的数据开发、分析和数据库运维中&#xff0c;一款优秀的 IDE 能够极大地提升工作效率。DBEaver 是一款由 Java 编写的一站式跨平台连接器&#xff0c;其社区版本已能支持连接近百种数据库&#xff0c;受到广大开发者的喜爱。近期。DolphinDB 与 DBeaver 团队共同努力&…

【ArcGIS微课1000例】0138:ArcGIS栅格数据每个像元值转为Excel文本进行统计分析、做图表

本文讲述在ArcGIS中,以globeland30数据为例,将栅格数据每个像元值转为Excel文本,便于在Excel中进行统计分析。 文章目录 一、加载globeland30数据二、栅格转点三、像元值提取至点四、Excel打开一、加载globeland30数据 打开配套实验数据包中的0138.rar中的tif格式栅格土地覆…

JVM之垃圾回收器ZGC概述以及垃圾回收器总结的详细解析

ZGC ZGC 收集器是一个可伸缩的、低延迟的垃圾收集器&#xff0c;基于 Region 内存布局的&#xff0c;不设分代&#xff0c;使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记压缩算法 在 CMS 和 G1 中都用到了写屏障&#xff0c;而 ZGC 用到了读屏障 染色指针&a…

C# XPTable 日期字段处理(XPTable控件使用说明十三)

1、SQLite数据库定义为日期类型 2、XPtable中日期字段定义与显示 //显示时间表columnModel1.Columns.Clear();columnModel1.Columns.Add(new NumberColumn("id", 30));NumberColumn numberColumn new NumberColumn("次数", 50);numberColumn.Maximum 100…

【pycharm发现找不到python打包工具,且无法下载】

发现找不到python打包工具,且无法下载 解决方法&#xff1a; 第一步&#xff1a;安装distutils&#xff0c;在CMD命令行输入&#xff1a; python -m ensurepip --default-pip第二步&#xff1a;检查和安装setuptools和wheel&#xff1a; python -m pip install --upgrade …

晨辉面试抽签和评分管理系统之六:面试答题倒计时

晨辉面试抽签和评分管理系统&#xff08;下载地址:www.chenhuisoft.cn&#xff09;是公务员招录面试、教师资格考试面试、企业招录面试等各类面试通用的考生编排、考生入场抽签、候考室倒计时管理、面试考官抽签、面试评分记录和成绩核算的面试全流程信息化管理软件。提供了考生…

王炸组合:Dolphinscheudler 3.1.*搭配SeaT unnel2.3.*高效完成异构数据数据集成

概述 本篇主要介绍如何通过Dolphinscheduler海豚调度搭配Seatunnel完成异构数据源之间的数据同步功能&#xff0c;这个在大数据流批一体数仓建设的过程中是一个非常好的解决方案&#xff0c; 稳定高效&#xff0c;只要用上了你肯定爱不释手。 环境准备 dolphinscheduler集群…

【AI日记】25.01.11 Weights Biases | AI 笔记 notion

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】【读书与思考】 AI kaggle 比赛&#xff1a;Forecasting Sticker Sales笔记&#xff1a;我的 AI 笔记主要记在两个地方 有道云笔记&#xff1a;数学公式和符号比较多的笔记notion&#xff1a;没什么数学公式的…

Oracle EBS GL定期盘存WIP日记账无法过账数据修复

系统环境 RDBMS : 12.1.0.2.0 Oracle Applications : 12.2.6 问题症状 用户反映来源为“定期盘存”和类别为“WIP”的日记账无法过账,标准日记账的界面上的过账按钮灰色不可用。但是,在超级用户职责下,该日记账又可以过账,细心检查发现该业务实体下有二个公司段值15100和…

欧拉路径算法

欧拉图&#xff1a; 对于应该连通图G&#xff0c;有&#xff1a; 1欧拉路径&#xff1a;一条路径&#xff0c;它能够不重复地遍历完所有的边&#xff0c;这个性质很像不重复地一笔画完所有边&#xff0c;所以有些涉及到欧拉路径的问题叫做一笔画问题。 2欧拉回路&#xff1a…

【进程与线程】程序和进程在内存中的表现

在计算机系统中&#xff0c;程序和进程是两个密切相关但又有本质区别的概念&#xff0c;尤其在内存中的表现上有显著不同&#xff1a; 在这张图中可以直观地看出程序和进程在内存中的结构区别。 基本定义 程序 程序 是一个 静态实体&#xff0c;表示一组写好的指令和数据的…

“多维像素”多模态雷视融合技术构建自动驾驶超级感知能力|上海昱感微电子创始人蒋宏GADS演讲预告

2025年1月14日&#xff0c;第四届全球自动驾驶峰会将在北京中关村国家自主创新示范区展示交易中心-会议中心举行。经过三年的发展&#xff0c;全球自动驾驶峰会已经成长为国内自动驾驶领域最具影响力、规模最大的产业峰会之一。昱感微电子创始人&CEO受到主办方邀请&#xf…

Linux创建server服务器实现多方信息收发

一&#xff0c;服务端 1.创建socket套接字&#xff0c;用于网络通信&#xff0c;同一台机器上的进程也可以通过本地套接字进行通信 //1.socket s_fd socket(AF_INET,SOCK_STREAM,0); if(s_fd -1){ perror("socket"); exit(-1); } //server address s_addr.sin_fami…

UML系列之Rational Rose笔记七:状态图

一、新建状态图 依旧是新建statechart diagram&#xff1b; 二、工作台介绍 接着就是一个状态的开始&#xff1a;开始黑点依旧可以从左边进行拖动放置&#xff1a; 这就是状态的开始&#xff0c;和活动图泳道图是一样的&#xff1b;只能有一个开始&#xff0c;但是可以有多个…

jsx语法中el-table-v2中cellRender如何使用动态绑定

答案&#xff1a;:attribute"xx"改为attribute{xx} 改写&#xff1a; const columns ref([{ key: index, dataKey: index, title: t(setting.index), width: 100 },{ key: no, dataKey: no, title: t(setting.key), width: 100 },{ key: name, dataKey: name, tit…