首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >C++11『右值引用 ‖ 完美转发 ‖ 新增类功能 ‖ 可变参数模板』

C++11『右值引用 ‖ 完美转发 ‖ 新增类功能 ‖ 可变参数模板』

作者头像
北 海
发布于 2023-11-17 02:34:42
发布于 2023-11-17 02:34:42
70200
代码可运行
举报
运行总次数:0
代码可运行

🌇前言

自从C++98以来,C++11无疑是一个相当成功的版本更新。它引入了许多重要的语言特性和标准库增强,为C++编程带来了重大的改进和便利。C++11的发布标志着C++语言的现代化和进步,为程序员提供了更多工具和选项来编写高效、可维护和现代的代码


🏙️正文

1.右值引用

右值引用C++11 的重大更新之一,它的出现很好的解决了 临时资源浪费 的问题,同时也给 类和对象 做了一个全面升级,使其能轻松规避很多低效拷贝问题

1.1.什么是右值引用?

在学习 右值引用 之前,需要先来看看 左值引用引用C++ 相对于 C语言 的升级点之一,引用 既能像指针那样获取资源的地址,直接对资源进行操纵,也不必担心多重 引用 问题,对于绝大多数场景来说,引用指针 好用得多

而我们之前使用的所有引用都称为 左值引用,主要用于引用各种 变量,如果想引用 常量,需要使用 const 修饰

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 左值引用
int main()
{
	int a = 10;

	// 引用变量
	int& ra = a;

	// 引用 常量/临时对象
	const int& rb = 10;
	const int& rc = int();
	return 0;
}

C++11 中,新增了 右值引用 的概念,就是将 左值引用 中的 & 变为 &&右值引用 可以直接引用 左值引用 中需要加 const 引用的值;也可以通过函数 move 引用 左值引用 直接引用的值

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 右值引用
int main()
{
	int a = 10;

	// 引用 常量/临时对象
	int&& rrb = 10;
	int&& rrc = int();

	// 引用变量
	int&& ra = move(a);
	return 0;
}

其中,诸如 「变量 / 数组元素 / 解引用后的指针」 等,在表达式结束后仍然存在、并且可以被取地址的值称为 左值;而 「常量 / 临时对象 / 表达式结果」 等,在表达式结束后即将被销毁的临时对象,或者无法被直接取地址的值称为 右值

快速判断 左值 / 右值 的方法之一就是 看看能不能取地址

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 判断左值 / 右值
int main()
{
	int a = 10;

	// 左值
	cout << &a << endl;

	// 右值
	cout << &10 << endl; // 【报错】
	cout << &int() << endl; // 【报错】
	return 0;
}

直接可以引用 左值 的称为 左值引用,直接可以引用 右值 的就是 右值引用

注意:

  1. 左值引用 可以通过其他手段引用 右值,比如加 const右值引用 也可以通过其他手段引用 左值,比如 move 函数
  2. 赋值语句左边的一定是 左值,但右边的不一定是 右值,比如 int a = b
1.2.move 转移资源

无论是 左值引用 还是 右值引用,本质上都是在给 资源 起别名,当 左值引用 引用 左值 时,是直接指向 资源,从而对 左值 进行操作;当 右值引用 引用 右值 时,则是先将 常量 等即将被销毁的临时资源 “转移” 到特定位置,然后指向该位置中的 资源,对 右值 进行操作

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int a = 10;

// 左值引用 引用 左值
int& ra = a;

// 右值引用 引用 右值
int&& rr = 10;

正因为将资源 “转移” 了,右值引用 才可以对资源进行利用

所以虽然 右值引用 引用的是 右值,但 右值引用 本身是可以取地址的,比如 &rr 是可以的,毕竟 rr 也指向了一块空间,这块空间中存储的是临时资源,这也就意味着 右值引用 是可以对临时资源进行修改操作的,也就是将临时资源再利用

对于 「常量 / 临时对象 / 表达式结果」右值编译器会直接转移资源,但对于用户自定义的 左值,编译器不敢轻举妄动,只敢给用户提供一个 转移变量资源 的函数 move,有了 move 之后,右值引用 就能引用 左值

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int a = 10;

// 左值引用 引用 右值
const int& r = 10;

// 右值引用 引用 左值
int&& rr = move(a);

语法还支持给 右值引用const,这样做的含义是 不能修改右值引用后的值

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main() 
{
	int a = 10;

	const int&& crr = 10;
	const int&& crra = move(a);

	++crr; // 【报错】
	++crra; // 【报错】

	return 0;
}

一般情况下是不会这样干的,右值引用 是为了移走资源,加了 const 还不如直接改用 const 左值引用


不要轻易使用 move 函数,左值 中的资源可能会被转走,在 C++11 之后,几乎所有的 STL 容器都增加了一个 移动构造 函数,其中就用到了 右值引用

如果此时我们直接将 左值 move 后构造一个新对象,会导致原本左值中的 资源 丢失

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// move 转移资源
int main()
{
	string str = "Hello World!";
	cout << "str: " << str << endl;

	// 使用 move 函数后
	string tmp = move(str);
	cout << "str: " << str << endl;

	return 0;
}

所以一般情况下不要轻易使用 move 移动函数,除非你确定该资源后续不再使用

1.3.左值引用 vs 右值引用

C++11 之前,使用 const 左值引用 也可以引用 右值,并且在我们之前的学习中只使用 左值引用 也没什么大问题啊,那为什么还要搞出一个 右值引用 呢?

答案是 右值引用可以提高资源的利用率,进而提高整体效率

有了右值引用之后,之前只能 【读取】、【拷贝】的临时资源变得更有价值了,可以在右值引用后进行操作,也可以将资源转移以减少拷贝

下面是 左值引用右值引用 的对比图

特征

左值引用

右值引用

语法

Type& lvalueRef = variable;

Type&& rvalueRef = std::move(variable);

绑定对象

现有对象

临时对象或可移动对象

典型用途

函数参数、返回类型

移动语义、完美转发

示例

int x = 10; int& ref = x;

int&& rref = 10;

可重新赋值

可为 nullptr

是(需谨慎使用)

引用折叠(C++11)

Type&& && 折叠为 Type&&

生命周期延长(C++20)

否(延长临时对象的生命周期)

是(绑定到临时对象,如果绑定到右值则延长生命周期)

注意: 表格提供了一个高层次的概述,实际上有更多的细节和差异,尤其是在C++的后续版本中引入的一些特性

1.4.右值引用的使用场景

右值 分为

  • 纯右值(内置类型)
  • 将亡值(自定义类型)

纯右值 价值不大,但 将亡值 就不一样了,直接转移 将亡值 资源可以减少 拷贝次数,所以 右值引用 的使用场景主要体现了 拷贝

下面是简单模拟实现的 string,其中并未涉及 右值引用 相关知识

简单模拟实现的 string

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
namespace Yohifo
{
	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];
			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;
			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()
		{
			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)
		{
			push_back(ch);
			return *this;
		}

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

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

	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

为了更好的观察是否发生了 深拷贝行为,在 拷贝构造 函数中加入了对应的打印语句,这里的参数为 const 左值引用

主函数中测试 左值右值 两种拷贝构造

主函数 main.cpp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	Yohifo::string str = "Hello World!";

	// str 为左值
	Yohifo::string s1 = str;

	// str+'\n' 为右值
	Yohifo::string s2 = str + '\n';

	return 0;
}

为什么要加 \n防止编译器直接将 拷贝构造 优化为 直接构造

首先是测试 C++11 之前的结果(没有 右值引用

可以看到这里发生了 3深拷贝 行为,其中一次为 str 拷贝构造,一次为 str + '\n' 拷贝构造,还有一次是 operator+() 函数中的拷贝行为(无法避免)

现在足以证明,在没有使用 右值引用 的情况下,即便是传入 右值,触发的也是 深拷贝,浪费了 右值 这个临时资源

注意: 如果此时只显示了两次深拷贝,那是因为 VS 的平台工具集 v143 存在问题,会将 str+'\n' 这次拷贝构造优化掉,解决方法就是将平台工具集改为 v142

接下来在 string 中重载一个 拷贝构造 函数,参数为 右值引用,此时称为 移动构造

移动构造 string() — 位于 string

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动构造" << endl;
	swap(s);
}

编译后再次运行,可以看到此时少了一次 深拷贝,多了一次 移动构造

移动构造 是由 str+'\n' 拷贝构造时触发的,又因为参数是 右值(临时对象),所以这里的 string 对象只需与 “右值” 进行 swap 就行了

可以通过调试证明 s2 的资源是从其他地方 “转移” 过来的


如今的编译器都很智能,会自动进行优化以减少拷贝,比较典型的就是 构造 + 拷贝构造 优化为直接构造,那么对于 移动构造 编译器是否会做出优化?

为了模拟优化场景,这里简单实现一个 to_string,目的是为了在函数结束后返回一个 临时对象

整型转为字符串 to_string() — 位于命名空间 Yohifo

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
string to_string(int val)
{
	bool flag = false;
	if (val < 0)
	{
		flag = true;
		val *= -1;
	}

	string ret;
	while (val)
	{
		int n = val % 10;
		ret += n + '0';
		val /= 10;
	}

	if(flag)
		ret += '-';

	std::reverse(ret.begin(), ret.end());

	return ret;
}

主函数中就负责调用 to_string() 获得一个临时对象,然后通过该临时对象去构造一个对象

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	Yohifo::string str(Yohifo::to_string(100) + 'a');

	return 0;
}

首先来看看 C++11 之前(屏蔽 移动构造

结果为 3 次深拷贝

分析:第一次为 to_string() 函数执行完后返回的临时对象的拷贝,第二次为 operator+() 函数中生成的临时对象(不可避免),第三次为 strto_string() + 'a' 形成的临时对象的拷贝

实际拷贝次数不止 3 次,就拿 to_string() 函数来说,需要先将 ret 拷贝给 临时对象,再将 临时对象 拷贝给 调用者,编译器在这里会优化,优化成一次拷贝构造:ret 拷贝给调用者

这里的 深拷贝 是可以避免的,现在重新启用 移动构造 函数,再看看结果

可以看到 3深拷贝 变成了 2移动构造 + 1深拷贝(不可避免)

分析:第一次拷贝的对象是 临时对象(右值),资源即将销毁,触发 移动构造,将资源及时转移;第三次拷贝也是如此,同样可以通过 移动构造 将临时对象资源转移

对于 to_string() 函数来说,也不应该只发生一次 移动构造,实际应该先把 ret 拷贝给 临时对象,再将 临时对象 中的资源转移;但编译器判断 ret 是一个局部变量,出了函数就销毁了,于是就优化成了 return move(ret); 函数返回时将 ret 中的资源通过 move 函数转移

由此可以看出,编译器会在 临时对象 当作中间人连续赋值的场景中,直接将 临时对象 优化掉,尽量减少拷贝,这才有了 to_string() 函数中最终看到的 一次拷贝构造 / 一次移动构造

言归正传,得益于 移动构造临时对象 的资源得到了回收利用,传值返回时不再需要经过无意义且低效的 深拷贝

这里只是一个小小的 string,如果是 vectormapunordered_map 等基于模板的复杂容器,移动构造 带来的效率提升是非常显著的


关于 移动构造 相关问题

Q1:能否将函数返回值设为 右值引用?

答案是 不行,不是说单纯的 右值引用 解决了 无效深拷贝 问题,而是基于 右值引用 实现的 移动构造 解决了问题,所以无论是 右值引用 还是 左值引用,在面对 传值返回时,都不能作为函数返回值类型,返回局部对象引用会导致程序异常退出

并且在使用 右值引用 作为返回类型时,需要手动把 ret 这个左值 move,否则无法编译(右值引用不能直接引用左值),即使编译通过了,运行后也是有问题的

有问题的函数 to_string()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
string&& to_string(int val)
{
	bool flag = false;
	if (val < 0)
	{
		flag = true;
		val *= -1;
	}

	string ret;
	while (val)
	{
		int n = val % 10;
		ret += n + '0';
		val /= 10;
	}

	if(flag)
		ret += '-';

	std::reverse(ret.begin(), ret.end());

	return move(ret);
}

可以看到,不仅没有触发 移动构造,还迫使程序异常终止(退出码不为 0

Q2: 函数传值返回,但在返回时能否手动 move 返回值?

答案是 可以的,前面说过,编译器优化后,会自动给返回值加上 move 以取出其中的资源,所以这里手动加上也没问题,但没必要

结果也是正常的

Q3: 右值引用什么时候作为参数类型使用?

当传入的参数为 右值 时,推荐使用 右值引用 作为参数类型;如果既有传入 左值 也有传入 右值 的情况,可以重载一个 右值引用 参数版本,编译器会匹配最合适的版本,确保资源不被浪费

常见的 右值引用 作为参数类型的有:拷贝构造函数赋值重载函数(这两个函数都是重载版本),传值拷贝是比较低效的行为,有了这两个函数后, 中其他函数可以放心传值返回

力扣题目 「杨辉三角」中的函数返回值为 vector<vector<int>>,只要 vector 中实现了 移动构造 函数,就可以避免深拷贝,轻松返回结果

1.5.右值引用的意义

右值引用 是个好东西,它的核心功能在于再次利用 临时资源,避免无意义且低效的拷贝行为

右值引用左值引用 各有各的适用场景:左值引用 是引用返回以提高效率(减少拷贝);右值引用 则是移动构造提高效率(减少拷贝),两者的角度不同

  • 左值引用:直接引用对象以减少拷贝
  • 右值引用:间接减少拷贝,将临时资源等将亡值的资源通过 移动构造 进行转移,减少拷贝

2.完美转发

泛型编程C++ 中的核心功能之一,典型的让程序员少走弯路,让编译器多干活,伴随着 右值引用 的新概念加入,泛型编程 也需要随之升级

2.1.模板中的万能引用

泛型编程 的核心在于 模板根据参数类型推导函数,当我们分别传入 左值引用右值引用 时,模板 是否能正确推导呢

下面这段代码的含义是 分别传入 左值const 左值右值const 右值,并设计对应参数的回调函数,将参数传给模板,看看模板是否能正确回调函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void func(int& a)
{
	cout << "func(int& a) 左值引用" << endl;
}

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

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

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

template<class T>
void perfectForward(T&& val)
{
	// 调用函数
	func(val);
}

int main()
{
	int a = 10;
	const int b = 10;

	// 左值
	perfectForward(a);
	perfectForward(b);

	// 右值
	perfectForward(move(a));
	perfectForward(move(b));

	return 0;
}

注:move(const 左值) 可以获取 const 右值

模板中涉及引用参数传递时,可以将函数参数类型写为 T&&,因为模板具有自动推导的特性,当传入的参数为 左值 时,触发 引用折叠 机制,实际参数类型会变为 T&;当传入的参数为 右值 时,正常使用 T&& 就行了 这一机制在模板中称为 万能引用(引用折叠),既能推导 左值引用,也能推导 右值引用

预期结果:先调用 左值引用const 左值引用 版本的 func,再调用 右值引用const 右值引用 版本的 func

实际运行结果如下

调用的全是 左值引用 相关的 func,难道一向靠谱的模板推导出现问题了吗?

当然不是模板 是根据我们传入的参数类型,来推导出相应的函数,如果说 模板 推导没有问题,那问题就出在 回调函数 的参数上了,只有推导后,无论传的 左值 还是 右值,编译器都会把 val 变为 左值,这样才能解释为什么最终结果全部为 左值引用const 左值引用

编译器这么做合理吗?

非常合理,首先要明白 右值 是无法被取地址的,而 右值引用 是将 右值 中的资源转存到一块特定的空间中,这也就意味着 右值引用 后的值,必定是一个 左值(拥有空间,可取地址),只有为 左值 才可以对其进行修改等操作

简单来说就是 右值属性转早了

解决问题的核心在于 perfectForward 传递 val 参数时,如何保证它的 右值属性 不丢失

2.2.传参过程中保持右值属性

要想在参数传递过程中保持其 右值属性,就需要使用 forward 函数,也就是 完美转发

forward 是一个带有参数模板的函数,主要在传参时使用: 如果参数原本是右值,但在右值引用后失去了右值属性,使用 forward 函数可以恢复它的右值属性

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class T>
void perfectForward(T&& val)
{
	// 调用函数
	func(forward<T>(val));
}

再次运行程序,可以发现调用结果符合预期

注意: forward 是一个模板函数,需要指定模板参数类型 T,确保能正确推导并传递

2.2.完美转发实际应用

完美转发 在实际开发中会经常用到,前面说过,在 C++11 之后,所有的类都可以新增一个 移动构造 以规避无意义的低效拷贝行为,并且由于大部分类中会涉及 模板 的使用,保持右值属性 就是一个必备的技巧,如果没有 完美转发,那么 移动构造 顶多也就减少了一次 深拷贝

接下来看看 完美转发 如何应用

首先准备一个模拟实现的 list

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#pragma once
#include<assert.h>

namespace Yohifo
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		list_node(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};

	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* n)
			:_node(n)
		{}

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

		Ptr operator->()
		{
			return &_node->_data;
		}

		self& operator++()
		{
			_node = _node->_next;

			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		self& operator--()
		{
			_node = _node->_prev;

			return *this;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}

		bool operator==(const self& s)
		{
			return _node == s._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);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

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

		const_iterator end() const
		{
			return const_iterator(_head);
		}

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

		list()
		{
			empty_init();
		}

		template <class Iterator>
		list(Iterator first, Iterator last)
		{
			empty_init();

			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		
		void swap(list<T>& tmp)
		{
			std::swap(_head, tmp._head);
		}

		list(const list<T>& lt)
		{
			empty_init();

			list<T> tmp(lt.begin(), lt.end());
			swap(tmp);
		}

		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				//it = erase(it);
				erase(it++);
			}
		}

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

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		void insert(iterator pos, const T& x)
		{
			node* cur = pos._node;
			node* prev = cur->_prev;

			node* new_node = new node(x);

			prev->_next = new_node;
			new_node->_prev = prev;
			new_node->_next = cur;
			cur->_prev = new_node;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			node* prev = pos._node->_prev;
			node* next = pos._node->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;

			return iterator(next);
		}
	private:
		node* _head;
	};
}

因为在构建链表节点时,是不需要 深拷贝 的,可以给 节点类 增加 移动构造函数

新增链表节点的移动构造 list_node — 位于 list_node

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{}

主函数中只需创建一个 list<string> 对象,,查看 移动构造是否被正确调用

注意: 这里的 liststring 都是模拟实现的

测试移动构造是否生效

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	Yohifo::list<Yohifo::string> l;
	l.push_back("Hello World!");

	return 0;
}

执行结果为 两次深拷贝

第一次深拷贝为构造时触发(默认构造传的是 右值),第二次则是插入时触发(插入的也是 右值

这里在 构造 / 插入 时使用的可是 右值 啊,为什么 string 中的 移动构造 函数没有被正确调用呢?进入调试模式,发现第一个问题:没有给 list 提供右值引用版本的 push_back()

这里先提供一个 右值引用版本push_back(),并在参数传递时使用 完美转发,看看能不能解决问题

右值引用版的 push_back() — 位于 list

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 右值引用版
void push_back(T&& x)
{
	// 完美转发
	insert(end(), std::forward<T>(x));
}

结果仍然是两次 深拷贝

原因是因为 push_back() 并没有干实事,它自己也在调用 insert(),而 insert() 还没有提供 右值引用 版,这里先试着补上

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 右值引用版
void insert(iterator pos, T&& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	// 完美转发
	node* new_node = new node(std::forward<T>(x));

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

已经增加了两次 完美转发 了,结果仍是两次 深拷贝

仔细观察 insert() 的代码可以发现,在插入节点之前,需要先构建一个 node 节点对象,构建对象时已经进行了 完美转发,意味着当前参数传递没有问题,顺着线索来到 node移动构造 函数中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{}

其中的 _data 也就是 string 对象,在构造时,是直接传递了 x,并没有对其进行 完美转发,从而导致最终传给 string 的是一个 左值,自然调用的就是 深拷贝 了,话不多说,再加上一次 完美转发

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(std::forward<T>(x))
{}

再次运行程序,发现这次终于成功调用了 string移动构造 函数


要想让我们之前模拟实现的 list 成功进行 移动构造,需要增加:一个移动构造、两个右值引用版本的函数、三次完美转发,并且整个 完美转发 的过程是层层递进、环环相扣的,但凡其中有一层没有进行 完美转发,就会导致整个传递链路失效,无法触发 移动构造

所以对于这种涉及多次函数回调的类,需要确保 右值 传递的每一层都不会丢失 右值属性,否则 移动构造 就断了


3.新增类功能

C++11 中新增了 右值引用 + 移动语义,应用到类中就诞生了 移动构造移动赋值 函数,除此之外,还对类中参数可能为 右值 的函数重载了 右值引用 版本

3.1.移动构造和移动赋值

之前类中有六个天选之子:构造函数、析构函数、拷贝构造、赋值重载、取地址重载 和 const 取地址重载

有了 右值引用 + 移动语义 后,对 拷贝构造赋值重载 进行了 “升级”,增加了 移动构造移动赋值 这两个新函数,至此,类中共有八个天选之子(编译器会默认生成)

天选之子 的意思就是 即使我们不写,编译器也会默认生成(有条件)

之前六个 天选之子 的生成规则这里就不再阐述了,主要来说说 移动语义 相关的两个函数

移动语义就是通过右值引用将资源转移再利用

这两个函数生成的条件比较苛刻:

  1. 如果自己没有写 移动构造 ,并且没有实现 析构拷贝构造赋值重载 中的任意一个,那么编译器才会自动生成一个 移动构造 函数,移动构造 函数对于内置类型,会按字节拷贝,对于自定义类型,会去调用它的 移动构造 函数,如果没有,就调用 拷贝构造(目的:涉及深拷贝的类编译器期望我们自己设计 移动构造 函数)
  2. 移动赋值 的生成逻辑与上面一致

编译器为什么会这么要求?

如果我们实现了 析构、拷贝构造、赋值重载,就证明当前的类中涉及到了 动态内存管理,是需要自己进行 深拷贝 的,编译器无能为力,移动语义 也应该根据自己的实际场景进行设计,所以编译器就没有自动生成

如何自己实现这两个 移动语义 相关函数?

得益于 右值引用,这个实现起来并不复杂,以 string 为例,移动构造移动赋值 的实现如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	swap(s);
}

// 移动赋值
string& operator=(string&& s)
{
	swap(s);
	return *this;
}

核心在于 与临界资源(将亡值)交换资源

默认生成的 移动构造 或者 移动赋值 并非没有用,就像 拷贝构造 一样,默认生成的拷贝构造会去调用该函数中涉及类的 拷贝构造,也就是说,只要底层类没问题,自动生成的函数也可以实现 深拷贝 / 移动构造 / 移动赋值


如果非要使用编译器默认生成的呢?

在想让编译器生成的函数之后加上 default 关键字,如果类中涉及 动态内存管理(比如这里的 string,是不推荐使用默认生成函数的,因为会涉及到 深拷贝

并且由于 移动构造 属于 构造 家族,移动赋值 属于 赋值 家族,移动构造 / 移动赋值 存在的前提是 拷贝构造 / 赋值重载 也存在,如果都使用默认的,自然就无法 深拷贝


STL 中的容器都增加了 移动构造移动赋值

3.2.插入系列的重载版本

除了 构造 / 赋值 时提高效率,插入 时也能提高效率,也就是通过 右值引用 重载实现 移动语义 版的 插入函数

比如之前实现的 list

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 右值引用版
void insert(iterator pos, T&& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	// 完美转发
	node* new_node = new node(std::forward<T>(x));

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

注意: 如果移动语义版的插入函数中涉及函数回调、构造对象等,就需要使用 完美转发 保持右值的属性,确保能成功调用移动语义版本的函数


STL 中同样更新了一波 移动语义 版的 插入函数

说到底 移动语义 其实就是通过 右值引用 进行资源转移的行为

移动语义是否能延长临时对象(将亡值)的生命周期? 不能,只是将其中的资源转移了,但临时对象(将亡值)本身仍然会被销毁

const 引用延长生命周期问题 这是 C++11 之前对于右值的处理手段,在 push_back() 等插入函数值,常常会传入一个临时对象,此时就可以使用 const 引用作为参数类型来延长临时对象的生命周期,伴随 push_back() 栈帧销毁而被销毁


注意不要认为 const 引用做返回值时能延长局部对象的生命周期,局部对象出了作用域就被销毁了,而 const 引用此时指向被销毁的对象,这是不合理的,是一种类似 “野指针” 的 “野引用” 行为

3.3.新增关键字

default 关键字

可以指定编译器生成默认的函数,比如在下面这个类 Test 中,我们指定编译器生成 构造拷贝构造

测试类 Test

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Test
{
public:
	// 构造
	Test() = default;
	
	// 拷贝构造
	Test(const Test&) = default;

private:
	Yohifo::string _str;
};

这里的是 string 是之前模拟实现的,方便查看调用的是 深拷贝 还是 移动构造

分别传入 左值右值 查看函数调用情况

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	Test t1;

	Test t2(t1); // 传入左值
	Test t3(move(t1)); // 传入右值
	return 0;
}

可以看到当前两次都是 深拷贝,可以推断出编译器并没有给 Test 自动生成 移动构造,原因在于我们已经指定生成了 拷贝构造,编译器认为 Test 类中不具备自动生成 移动构造 的条件

可以使用 defalut 指定编译器自动生成 移动构造

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Test(Test&&) = default; // 指定生成移动构造

再次运行程序,可以看到当传入 右值 进行构造时,调用的是 移动构造

这里想强调的是 default 可以指定编译器自动生成类中的默认成员函数

能否使用 default 生成除默认成员函数之外的其他成员函数?

答案是 不行,如果这都可以的话,编译器都能自动写代码了,能自动生成默认成员函数,是因为这些函数的实现方式都是有模板的,编译器可以直接套用


delete 关键字

除了 default 关键字,C++11 还提供了 delete 关键字,用法和 default 一样,不过 delete 是声明该函数已被手动删除,不可以使用,比如将 Test 中的 构造 函数删除,就无法构造对象了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 删除构造函数
Test() = delete;

什么情况下需要删除函数?

比如在 单例模式 中,只允许创建一个对象,为了避免外部再次创建对象,需要将 构造、拷贝构造、移动构造 等函数删除;再比如 C++ 中的 IO 流类中,是不允许 IO 对象之间进行拷贝的,因为每个 IO 对象中的缓冲区都不一样,随意拷贝会造成资源混乱,索性直接删除了


至于 finaloverride 已经在 继承和多态 相关章节介绍过了

  • final 修饰类,类不能被继承
  • final 修饰成员函数,子类继承时,成员函数不能被重写
  • override 修饰子类虚函数,确保完成重写

更多新增关键字详见 C++11 官网

3.4.其他新功能

C++11 还修复之前 中的一个大坑:内置类型不会初始化

这就导致如果你没有在编写 构造 函数时对 内置类型 进行处理,会导致后续使用时出现 随机值

比如下面这个类中就没有对 内置类型 进行处理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}

private:
	int _a;
};

int main()
{
	A a;
	a.Print();

	return 0;
}

打印结果为 随机值

使用随机值的危害? 如果将随机值作为循环起始值,会导致循环 “失控”

像这种大坑,估计是 C++ 独有的,为了修复这个问题,C++11 中新增了一个小补丁:类成员变量初始化

就是在类成员定义时,允许给一个 缺省值,比如这样

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class A
{
	// ...
	
private:
	int _a = 0; // 此时给的是缺省值
};

此时输出的结果就是可预期的

注意: 这里给的是 缺省值,成员变量最终都是在 初始化列表中 进行初始化的,定义时给缺失值,就可以在初始化列表中使用


C++11 中还新增了 委托构造,就是允许在 初始化列表 中调用构造函数,这个语法作用并不是很大,并且不推荐使用,因为进入初始化列表就已经表示正在初始化了,再去调用其他构造函数会显得调用逻辑混乱

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class A
{
public:
	A(int a = 0)
		:_a(a)
	{}

	// 拷贝构造时,进行委托构造
	A(const A& a)
		:A()
	{
		// ...
	}

private:
	int _a;
};

注意: 只有 构造 相关函数才有 初始化列表,其他函数没有这个东西,自然也就不能使用委托构造


4.可变参数

C++11 引入了 可变参数模板可变参数包 的特性,允许定义和使用可接受任意数量参数的模板函数,这对于编写泛型代码、容器等方面提供了更大的灵活性

4.1.可变参数列表

C 语言就已经出现了 可变参数,语法表示为 ...C语言中的输入输出函数就用到了 可变参数列表

可变参数 的意思是你可以随便传入多个 参数,函数都能进行接收,C语言在使用 可变参数模板 时需要依赖 参数数量 + 参数类型 来进行识别,简单使用如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	int a;
	double b;
	char c;
	scanf("%d %lf %c", &a, &b, &c);

	printf("输入了: %d %lf %c\n", a, b, c);
	return 0;
}

虽然这里也支持接收任意数量的参数,但还得提前确定这些参数的类型,使用起来比较麻烦

4.2.可变参数包

C++11 之前只能像 C语言 那样使用固定参数的 可变参数列表,在 C++11 中进行了重大改动,新增了 可变参数包,支持直接传入任意数量、任意类型的参数,不必像 C语言 那样指定数量和类型,这个改动非常激进,导致整个 可变参数 语法变得十分抽象

把所有传入的参数,不论数量、类型,统统进行打包,也就形成了 可变参数包

下面是使用 可变参数包 的实际例子(由于不知道会传入什么类型的参数,这里需要借助 可变参数模板

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class ...Args>
void showList(Args... args)
{
	// ...
}

为了提高参数传递时的效率,可变参数包 的类型一般都会写成 Args&&... 这在模板中称为 万能引用(引用折叠),既可以引用 左值,也可以引用 右值

可变参数模板 允许传入 任意数量、任意类型 的参数

比如下面这几种函数传参都是可以的,由此可见 可变参数模板 的强大

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	showList();
	showList(1, 2.2, 'c');
	showList("111111111111111");
	showList(vector<int>(), list<double>());
	return 0;
}
4.3.可变参数包的解析

可变参数模板 传参简单,可变参数包 解析就麻烦了,下面是一种不被编译器支持的错误解析方式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class ...Args>
void showList(Args... args)
{
	// 错误的解析参数方式
	int n = sizeof...(args);

	for (int i = 0; i < n; i++)
	{
		// 获取具体的可变参数
		args[i];
	}
}

注:使用 sizeof 计算可变参数包的大小时,需要在 sizeof 之后紧跟 ...,表示要计算的对象是可变参数包

这种解析方式很符合直觉,但编译器并不支持,具体报错信息为 必须在此上下文中扩展参数包

“上下文” 是一个抽象的术语,用于描述代码执行时所处的特定环境,这个环境可能是与函数调用相关的,也可能是其他方面的,这里的 上下文 具体指 模板的实例化和展开时的环境和情境

模板 的实例化和展开可以借助 递归 来实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 递归推导时结束时调用的函数
void showList()
{}

template<class T, class ...Args>
void showList(const T& val, Args... args)
{
	cout << val << " ";
	showList(args...); // 递归解析
}

int main()
{
	showList(1, 2.2, 'c');

	return 0;
}

可变参数包 的参数被成功解析了

因为是 递归 解析的,所以需要一个递归出口,也就是 参数为 void 的重载函数,推导逻辑如下

相关模板参数在编译阶段就已经全部推导出来了,也就是说当程序运行时,在当前代码中,会同时存在 4showList() 的重载函数,可以通过 __FUNCTION__ 这个和宏以及 sizeof 验证

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class T, class ...Args>
void showList(const T& val, Args... args)
{
	cout << __FUNCTION__ << "(" << sizeof...(args) << ")" << endl;
	showList(args...); // 递归解析
}

可以看到 可变参数模板 中的函数共被调用了 3 次,再加上 showList() 无参版的调用,总共就是 4 个重载函数

main 函数第一次调用时,1 被赋给了 valargs 参数个数变成了两个

除了这种 递归 解析参数包的方式外,还有一种奇特的解析方式 通过逗号表达式展开

具体实现如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class T>
void Print(T val)
{
	// 获取参数
	cout << val << " ";
}

template<class ...Args>
void showList(Args... args)
{
	int arr[] = { (Print(args), 0)... };
}

关键点在于 arr 数组创建时,会根据 { } 中的参数进行初始化,可以在此直接将 可变参数包展开,展开过程中就完成了 参数 的解析工作

为什么要写出成 (Print(args), 0) 的形式? 这是一个逗号表达式,目的是让整个式子最终返回 0,用于初始化 arr 数组

可以设置 Print() 的返回值来简化代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class T>
int Print(T val)
{
	// 获取参数
	cout << val << " ";

	return 0;
}

template<class ...Args>
void showList(Args... args)
{
	int arr[] = { Print(args)... };
}

编译后代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
template<class ...Args>
void showList(Args... args)
{
	int arr[] = { Print(1), Print(2.2), Print('c') };
}

这种参数包展开方式比较少用,简单了解即可


可变参数包 的应用场景在哪?

主要用于 线程回调函数 的参数传递,pthread 提供的线程创建接口 pthread_create 中只能给 线程回调函数 传递一个 指针变量C++11 中的 线程库 借助 可变参数包 进行了封装设计,可以在创建 线程 时轻易传递多个参数

注:这里的 Fn 是可调用的函数对象

关于 C++11 线程库 的更多知识将会放到下一篇文章中详谈

除此之外,可变参数包 还可以用于优化插入相关的函数

4.4.emplace 系列函数

C++11 还升级了 STL 中的插入函数(非右值引用版),这些新增的函数依赖 可变参数包,称为 emplace 系列

比如 listemplace_back()

empalce_back() 具备 push_back() 的所有功能,并且还在它的基础上进行了升级

如果只是单纯插入 左值 或者 move(左值),这两个函数没有区别

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	std::list<Yohifo::string> l;

	Yohifo::string str1 = "Hello";
	Yohifo::string str2 = "Hello";

	// 插入左值
	l.push_back(str1);
	l.emplace_back(str2);
	cout << endl;

	// 插入 move 出来的右值
	l.push_back(move(str1));
	l.emplace_back(move(str2));
	cout << endl;

	return 0;
}

但如果插入的是 纯右值,两个函数就有区别了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int main()
{
	//...
	
	// 插入纯右值
	l.push_back("World");
	l.emplace_back("World");

	return 0;
}

插入纯右值时,只发生了一次 移动构造

通过调试发现,emplace_back() 在插入 纯右值 "World" 时,甚至都没有调用 移动构造,而是直接走的 构造函数

得益于 可变参数包emplace 系列函数可以直接将 纯右值 作为参数传递,传递途中不展开参数包,直到 构造函数 才把参数包展开,充分发挥了 可变参数包 的优势(直接传递参数)

因此可以得出结论:在插入纯右值,并且构造函数能正常接收时,emplace 系列函数可以直接构造,省去了调用移动构造函数时的开销

为什么传递 "World" 可以直接构造? 因为当前模拟实现的 string 中,构造函数参数就是 const char*,可以直接将参数包中的参数进行传递

注意: 插入 左值 或者 move(左值) 时,emplace 系列函数和普通函数没区别


🌆总结

以上就是本次关于 C++11 中右值引用和移动语义的相关知识了,右值引用的引入解决了临时资源过度消耗的问题,为类添加了移动语义函数,同时也升级了插入函数以支持右值引用版本。可变参数包的引入简化了多参数传递,尤其在 C++11 线程库的使用中更为方便。新的 emplace 系列函数通过利用可变参数包,为类构造函数提供了更灵活的调用方式,进一步优化了代码的效率和可读性。这些更新使得 C++11 更加强大、灵活

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-11-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
1 条评论
热度
最新
大佬,为什么我跑出来的密码都是乱码
大佬,为什么我跑出来的密码都是乱码
回复回复点赞举报
推荐阅读
TDW设计周2023 | DAY1精彩回顾
TDW设计周由腾讯设计通道联合腾讯学堂共同举办,以设计峰会的活动形式,基于"DESIGN FOR GOOD”的理念,打造一年一度的腾讯设计盛会,旨在践行科技向善的理念,持续向外界传递腾讯设计的愿景和使命。
腾讯ISUX
2023/08/03
7350
TDW设计周2023 | DAY1精彩回顾
TDW设计周2023全攻略!
* 所有TDW票均可在当日10:00后入场QTX潮玩展参观,即TDW门票包含QTX门票。
腾讯ISUX
2023/08/03
5980
TDW设计周2023全攻略!
TDW设计周2023 | DAY3精彩回顾
2023年7月30日,TDW设计周2023完美收官!让我们一起回顾TDW设计周2023最后一天的现场吧~
腾讯ISUX
2023/08/03
5731
TDW设计周2023 | DAY3精彩回顾
TDW2022 腾讯设计周 精彩回顾
TDW2022腾讯设计周于11月18日完美收官! 腾讯设计周(TDW)由腾讯设计通道联合腾讯学堂共同举办,活动形式主要包括设计峰会和创意市集,今年设计周的主题是:「共创互生·设计向善」,延续"DESIGN FOR GOOD”的理念,开展一年一度的腾讯设计盛会。 今年的设计峰会共邀请了7位来自各个设计领域的嘉宾为大家分享自己的设计心得。为了让更多人能参与到本次设计周,除了线下,本次设计峰会我们还开通了同步直播,让热爱设计的每位同学不论身处何处都能参与到本次活动。 今年的创意集市共有18个摊位,为达到小投入大
腾讯云设计中心
2022/11/28
1K0
TDW2022 腾讯设计周 精彩回顾
TDW2022 腾讯设计周 精彩回顾
TDW2022腾讯设计周于11月18日完美收官! 腾讯设计周(TDW)由腾讯设计通道联合腾讯学堂共同举办,活动形式主要包括设计峰会和创意市集,今年设计周的主题是:「共创互生·设计向善」,延续"DESIGN FOR GOOD”的理念,开展一年一度的腾讯设计盛会。 今年的设计峰会共邀请了7位来自各个设计领域的嘉宾为大家分享自己的设计心得。为了让更多人能参与到本次设计周,除了线下,本次设计峰会我们还开通了同步直播,让热爱设计的每位同学不论身处何处都能参与到本次活动。 今年的创意集市共有18个摊位,为达到小投入大
腾讯ISUX
2022/11/22
5850
TDW2022 腾讯设计周 精彩回顾
倒计时!QQ潮玩展2021全攻略
QQ潮玩展倒计时开始!今年五一,顽鹅工厂释放无限潮力,打造顶级潮流艺术体验!顽鹅即将出厂,所有的CQQL PLAYERS,你准备好了吗? ▲ 点击海报购票 5月1日-5月3日,QQ潮玩展2021降落深圳(福田)会展中心。本次QTX,超重磅国际潮流艺术家联名展中展带来顶级艺术享受;200+潮玩品牌&设计师携巨量展会限定品亮相,满足玩家的收藏喜好;潮玩艺术家签售、QQ白模大赛创意展、集章打卡抽奖、线上直播专场……活动丰富,惊喜不断! QTX 2021以“顽鹅出厂 CQQL PLAYERS' SHOW”
腾讯ISUX
2021/04/27
1.9K0
【QTX潮玩展2022定档9月16-18日】八大福利重磅来袭,带你“潮”翻全场!
世界上很多美好的事物 经过了时间的酝酿和打磨 等待的结果,才会显得更加珍贵 一切美好,都终将如“期”而至 QTX潮玩展 2022 我们相逢于硕果累累的金秋九月 一切美好,终将如“期”而至 为响应和配合政府的防控疫情工作,保障展商及QTX粉丝朋友们的健康安全,在持续与多方政府单位与机构的反复沟通协调后,经过慎重的评估我们郑重决定: 原定于2022年8月19-21日在深圳会展中心(福田)举办的QTX潮玩展 2022将移师深圳国际会展中心(宝安),展会时间定档为2022年9月16日-9月18日。 开展时间
腾讯ISUX
2022/08/26
1.1K0
【QTX潮玩展2022定档9月16-18日】八大福利重磅来袭,带你“潮”翻全场!
TDW设计周2023 | 大咖特辑:Glenn Tan
来自马来西亚,是一位拥有丰富国际服务经验的资深创意人。曾就职于多家国际4A广告公司。服务客户包括:巴黎欧莱雅、可口可乐、微软、优衣库等品牌。并斩获One Show,D&AD等多项国际广告创意大奖。
腾讯ISUX
2023/08/03
3530
TDW设计周2023 | 大咖特辑:Glenn Tan
倒计时3天!QTX潮玩展2022全攻略
参会重要信息: 请您及时关注所在地和展会地防疫政策 并于11月23日24点前 在防疫“白名单”系统进行申报 未通过白名单不得入场! 扫码进入“白名单”登记系统 ↓↓↓ 请持身份证+三天三检(最后一检在深圳即可)+粤康码绿码+深圳24小时核酸入场。 QTX潮玩展倒计时3天!11月26日-11月28日,一年一度的潮玩盛会QTX潮玩展将在深圳国际会展中心(宝安)开展。 近20000㎡豪华展区,300+潮玩品牌惊喜亮相,沉浸式逛展,独一无二的展会体验,好玩、好买、好逛、全方位呈现全新的潮玩世界。 GO! ✔
腾讯ISUX
2022/11/24
8530
倒计时3天!QTX潮玩展2022全攻略
TDW设计周2023 | 大咖特辑:唐沐
曾任小米生态链副总裁、腾讯用户体验设计中心总经理。在腾讯期间创建用户体验设计中心 (CDC),负责 QQ、Qzone、QQ音乐等亿级用户的互联网产品用户体验;2013 年加入小米集团,带领小米核心研发团队开创智能家居生态,推出「小爱音箱」、「小米路由器」等超级爆品,销量过亿台,这些核心产品的探索,也帮助了小米在全球 AIoT 行业处于持续领先的位置。
腾讯ISUX
2023/08/03
4360
TDW设计周2023 | 大咖特辑:唐沐
探索沉浸城市——SIF 2019全球征集计划开启
3月1日下午,2019 青岛国际VR影像周——砂之盒沉浸影像展 (Sandbox Immersive Festival,简称SIF) 媒体发布会在“打磨场”贰贰零空间顺利召开,来自AR/VR/MR、影视、戏剧、音乐、建筑、新媒体艺术等多个领域的意见领袖参与了本次发布会。本届青岛国际VR影像周由国家广电总局支持、青岛市文旅局主办,众多专业机构、高校等共同参与。
VRPinea
2019/03/15
4640
【回顾】飓风(SIGGRAPH ASIA 2014)来袭,登陆深圳!
  由国际计算机学会(ACM - Association for Computing Machinery)举办的SIGGRAPH是世界上影响最广、规模最大,同时也是最权威的一个集科学、艺术、商业于一身的计算机图形和交互技术展览及会议,被称作计算机图形图像研究领域的“奥斯卡”。   汇聚了全球从事硬件、软件、电影与游戏产业、交互技术及教育等方面研究的专家及创意人员参会,同时将举办这些领域的展览,涵盖了从动漫和视觉效果、后期制作、移动图形学和交互应用,到CG研究、技术创新、行业动态等各个方面。   2008年S
腾讯高校合作
2018/03/19
1K0
【回顾】飓风(SIGGRAPH ASIA 2014)来袭,登陆深圳!
QTX潮玩展|PUPU ALIENS联名共创抢先看
QTX潮玩展将于2023.7.28-7.30暑期黄金周末档举办!30000㎡市中心(福田)超大场馆聚集500+知名潮玩艺术家。潮玩界知名IP联名合作,1000+限定品重磅首发!
腾讯ISUX
2023/08/03
4280
QTX潮玩展|PUPU ALIENS联名共创抢先看
SIGGRAPH ASIA 2014:在全球电脑图像盛会凸显的中国力量
本文转自杨静公众帐号杨静lillian   第七届SIGGRAPH ASIA亚洲电脑互动技术会议及展览于2014年12月3-6日在深圳会展中心举办。参会人员来自60多个国家或地区,总人数近6000人,其中国外代表人数超过800人。SIGGRAPH是由国际图形图像协会(ACM SIGGRAPH)举办的全球影响最广、规模最大、同时也是最权威的CG及互动技术展览及会议,是计算机图形图像研究领域的“奥斯卡”。2008年SIGGRAPH来到亚洲,本届SIGGRAPHAsia在深圳举办,也是SIGGRAPH Asia首
腾讯高校合作
2018/03/19
1.2K0
SIGGRAPH ASIA 2014:在全球电脑图像盛会凸显的中国力量
PUPU ╳ 奔腾小马在魔都车展的萌潮碰撞
故事发生在PUPU ALIENS的小伙伴们来到地球上之后…… 在浩瀚宇宙中,有一枚PUPU星球,拥有神奇力量的PUPU ALIENS就生活在这里,星球上还有火龙DINOO、金鸡CICI、金蛋EE、飞豚BOBO。 PUPU ALIENS坐着光速太空船PU-01从遥远星球而来,突然收到来自地球的信号,便以最快的速度抵达。为了适应重力的急速变化,大型飞船迅速变身为奔腾小马汽车。 在奔腾小马的陪同下,PUPU ALIENS到了上海国家会展中心的潮流车展。 这一次,是PUPU ALIENS第一次与汽车品牌进行合
腾讯ISUX
2023/04/21
4200
PUPU ╳ 奔腾小马在魔都车展的萌潮碰撞
QTX大看点! Daniel Arsham全系列AE作品展曝光!
2021年5月1日-3日,QQ潮玩展2021即将在深圳(福田)会展中心举办,倒计时已经启动,潮流艺术盛宴即将开始。 本次QQ潮玩展邀请到Archive Editions与QTX联名展中展,国际顶尖艺术家Daniel Arsham与Archive Editions合作的系列,即将在QTX现场重磅亮相! (展会地图,点击可放大) *如因特殊情况发生变动,请以展会当天公布为准 Daniel Arsham   QTX展中展:Archive Editions - 丹尼尔·阿尔轩 (Daniel Arsham
腾讯ISUX
2021/04/27
9170
TDW设计峰会首次免费公开,干货不断!
腾讯设计周(TDW)是由腾讯设计通道联合腾讯学院举办,活动形式主要由设计峰会和创意市集组成,基于“Design for Good”理念,旨在将TDW打造成一年一度设计行业践行向善理念的盛会,通过TDW的年度盛会持续对外界传递腾讯理念、愿景与使命。 2020腾讯设计周延续“Design for Good”的理念,设计峰会通过文化、用户、赋能、环境四个板块全面探索体验设计、创新及品牌、设计生态、设计赋能等相关主题,并首次对外免费公开的大会演讲。我们将与大家携手共进,为腾讯设计、为设计行业贡献自己的力量。
腾讯ISUX
2020/10/28
1.3K0
天狗鹅TENGUQ设计大揭秘
1.项目概述 | About This Project 在QQ21周年时,为纪念这特殊的日子,QQ潮玩推出了“大兽异志”系列的210体限量版哥是鹅SOFUBI潮玩,这也是QQ的第一款SOFUBI潮玩。“大兽异志”系列是以kaiju为题材的,主打SOFUBI材质的QQ系列潮玩。而在QQ22周年之际,“大兽异志”系列潮玩迎来了新的角色——天狗鹅/TENGUQ,天狗鹅是以传说中大天狗形象和QQ相结合。 2.设计过程 | Design Process 1.草图设计 为了和哥是鹅保持系列感,在设计上我们保留
腾讯ISUX
2021/04/29
6610
QTX重磅 | 顽鹅工厂幕后大BOSS“顽艺鹅”登场!
灵感流水线,潮流制造中! QQ潮玩展“幕后大BOSS” 顽艺鹅   隆重登场! 作为本次QTX的官方形象大使, 顽艺鹅化身成限定潮玩, 并于2021.5.1~5.3  QQ潮玩展现场三天  限量发售  金刚黑限定版  全球限量220体 QTX现场首发110体 (具体信息请看文末) 身为一只复古机械鹅, 顽艺鹅造型经典,简洁大气, 一秒唤起你有关童年的机器人回忆。 圆润的身躯搭配圆润的管道四肢, 头顶天线接收潮流第一手信息, 倔强眼神透露对艺术的执着, 红腰带是QQ经典红围巾化身..
腾讯ISUX
2021/04/15
8820
精彩回顾 | 原创设计论坛 2019 FALL
腾讯ISUX isux.tencent.com 社交用户体验设计 I♡UX原创设计论坛,由腾讯ISUX用户体验设计部旗下腾讯原创馆创办。 自2018年8月18日开启第一期以来,每个季度都会邀请国内外知名艺术家和设计大咖,畅谈有关艺术及设计的话题,为原创设计发声。 9月19日,I♡UX原创设计论坛再次震撼来袭,众多设计大咖云集腾讯滨海大厦,尽情分享,开心互动,场内欢笑不断,粉丝们表示“追星成功”。 我的设计师成长之旅 Chino Lam 上午的论坛环节,来自中国香港的著名潮玩设计师Chino
腾讯ISUX
2019/10/21
5860
精彩回顾 | 原创设计论坛 2019 FALL
推荐阅读
相关推荐
TDW设计周2023 | DAY1精彩回顾
更多 >
LV.1
深圳市腾讯计算机系统有限公司
目录
  • 🌇前言
  • 🏙️正文
    • 1.右值引用
      • 1.1.什么是右值引用?
      • 1.2.move 转移资源
      • 1.3.左值引用 vs 右值引用
      • 1.4.右值引用的使用场景
      • 1.5.右值引用的意义
    • 2.完美转发
      • 2.1.模板中的万能引用
      • 2.2.传参过程中保持右值属性
      • 2.2.完美转发实际应用
    • 3.新增类功能
      • 3.1.移动构造和移动赋值
      • 3.2.插入系列的重载版本
      • 3.3.新增关键字
      • 3.4.其他新功能
    • 4.可变参数
      • 4.1.可变参数列表
      • 4.2.可变参数包
      • 4.3.可变参数包的解析
      • 4.4.emplace 系列函数
  • 🌆总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档