当前位置: 首页 > article >正文

【C++笔记】C++11的深度剖析(二)

【C++笔记】C++11的深度剖析(二)

在这里插入图片描述

🔥个人主页大白的编程日记

🔥专栏C++笔记


文章目录

  • 【C++笔记】C++11的深度剖析(二)
    • 前言
    • 一.类型分类
    • 二.引用折叠
      • 2.1 完美转发
    • 三.可变参数模板
      • 3.1 基本语法及原理
      • 3.2 包扩展
      • 3.3 empalce系列接口
    • 后言

前言

哈喽,各位小伙伴大家好!上期我们讲了C++11的部分内容。今天我们继续深入探讨C++11。话不多说,我们进入正题!向大厂冲锋
在这里插入图片描述

一.类型分类

  • C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值
    (expiring value,简称xvalue)。
  • 纯右值是指那些字⾯值常量或求值结果相当于字面值或是⼀个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。
  • 将亡值是指返回右值引用的函数的调⽤表达式和转换为右值引用的转换函数的调用表达,如
    move(x)、static_cast<X&&>(x)相当于(X&&)x强转
  • 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

二.引用折叠

  • C++中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引用的引用。
  • 通过模板或 typedef 中的类型操作可以构成引用的引用时,这时C++11给出了⼀个引用折叠的规
    则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
  • 下面的程序中很好的展示了模板和typedef时构成引用的引用时的引用折叠规则,大家需要⼀个⼀个仔细理解⼀下。
  • 像f2这样的函数模板中,T&& x参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左
    值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。
  • Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{
	typedef int& lref;
	typedef int&& rref;
	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;
}
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;
	// a是左值,推导出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.1 完美转发

  • Function(T&& t)函数模板程序中,传左值实例化以后是左值引的Function函数,传右值实例化以后是右值引用的Function函数。
  • 但是结合我们前面讲的,变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使用完美转发实现。
  • 完美转发forward本质是⼀个函数模板,他主要还是通过引用折叠的方式实现,下面实例中传递给Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给Function的实参是左值,T被推导为int&,引用折叠为左值引⽤,forward内部t被强转为左值引用返回。
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(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;
}


三.可变参数模板

3.1 基本语法及原理

  • C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
    在这里插入图片描述

  • template <class… Args> void Func(Args… args) {} 传值参数包

  • template <class… Args> void Func(Args&… args) {} 左值引用参数包

  • template <class… Args> void Func(Args&&… args) {} 万能引用参数包

  • 我们用省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class…或typename…指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟…指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板⼀样,每个参数实例化时遵循引用折叠规则。

  • 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

  • 这里我们可以使用sizeof…运算符去计算参数包中参数的个数。

template <class... Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;
}
int main()
{
	double x = 2.2;
	Print(); // 包⾥有0个参数
	Print(1); // 包⾥有1个参数
	Print(1, string("xxxxx")); // 包⾥有2个参数
	Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
	return 0;
}
// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持
// 这⾥的功能,有了可变参数模板,我们进⼀步被解放,他是类型泛化基础
// 上叠加数量变化,让我们泛型编程更灵活。
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);

在这里插入图片描述

3.2 包扩展

包扩展就是把参数包拆解的过程。
注意包扩展的过程是在编译时,不是运行时。

  • 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展⼀个包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。
  • C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
 //可变模板参数
 //参数类型可变
 //参数个数可变
 //打印参数包内容
template <class ...Args>
void Print(Args... args)
{
	// 可变参数模板编译时解析
	// 下⾯是运⾏获取和解析,所以不⽀持这样⽤
	cout << sizeof...(args) << endl;
	for (size_t i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << " ";
	}
}
 void ShowList()
 {
	 // 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
	 cout << endl;
 }
 template <class T, class ...Args>
 void ShowList(T x, Args... args)
 {
	 cout << x << " ";
	 // args是N个参数的参数包
	 // 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
	 ShowList(args...);
 }
 // 编译时递归推导解析参数
 template <class ...Args>
 void Print(Args... args)
 {
	 ShowList(args...);
 }
 /*int main()
 {
	 Print();
	 Print(1);
	 Print(1, string("xxxxx"));
	 Print(1, string("xxxxx"), 2.2);
	 return 0;
 }*/
 //template <class T, class ...Args>
 //void ShowList(T x, Args... args)
 //{
 // cout << x << " ";
 // Print(args...);
 //}
 // Print(1, string("xxxxx"), 2.2);调⽤时
 // 本质编译器将可变参数模板通过模式的包扩展,编译器推导的以下三个重载函数函数
 //void ShowList(double x)
 //{
 // cout << x << " ";
 // ShowList();
//}
//
//void ShowList(string x, double z)
//{
// cout << x << " ";
// ShowList(z);
//}
//
//void ShowList(int x, string y, double z)
//{
// cout << x << " ";
// ShowList(y, z);
//}
//void Print(int x, string y, double z)
//{
// ShowList(x, y, z);
//}
 template <class T>
 const T& GetArg(const T& x)
 {
	 cout << x << " ";
	 return x;
 }
 template <class ...Args>
 void Arguments(Args... args)
 {}
 template <class ...Args>
 void Print(Args... args)
 {
	 // 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
	 Arguments(GetArg(args)...);
 }
 // 本质可以理解为编译器编译时,包的扩展模式
 // 将上⾯的函数模板扩展实例化为下⾯的函数
 // 是不是很抽象,C++11以后,只能说委员会的⼤佬设计语法思维跳跃得太厉害
 //void Print(int x, string y, double z)
 //{
 // Arguments(GetArg(x), GetArg(y), GetArg(z));
 //}
 int main()
 {
	 Print(1, string("xxxxx"), 2.2);
	 return 0;
 }


包扩展还有另一种方式。

3.3 empalce系列接口

  • template <class… Args> void emplace_back (Args&&… args);
  • template <class… Args> iterator emplace (const_iterator position,
    Args&&… args);
  • C++11以后STL容器新增了empalce系列的接口,empalce系列的接口均为模板可变参数,功能上兼容push和insert系列,但是empalce还支持新玩法,假设容器为container,empalce还支持直接插入构造T对象的参数,这样有些场景会更高效⼀些,可以直接在容器空间上构造T对象。
  • emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列
  • 第二个程序中我们模拟实现了list的emplace和emplace_back接口,这里把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前面说的empalce支持直接插入构造T对象的参数,这样有些场景会更高效⼀些,可以直接在容器空间上构造T对象。
  • 传递参数包过程中,如果是 Args&&… args 的参数包,要用完美转发参数包,方式如下std::forward(args)… ,否则编译时包扩展后右值引用变量表达式就变成了左值。

这里增加emplace系列直接这样写就可以。

void emplace_back(Args&&... args)
{
	insert(end(), std::forward<Args>(args)...);
}
template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
	Node* cur = pos._node;
	Node* newnode = new Node(std::forward<Args>(args)...);
	Node* prev = cur->_prev;
	// prev newnode cur
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
	return iterator(newnode);
}
template<class... X>
list_node(X&&... data)
	:data(forward<X>(data)...)
	, next(nullptr)
	, prev(nullptr)
{}

原理就是编译器根据可变模版参数生成对应参数的函数。

同时说明我们拿到参数包一定要包扩展吗?
不是,这里我们直接把参数包往下传就可以了。
当传到data这里时直接匹配对应的对应构造即可。需要包扩展。只有当用到参数包时再包扩展。

后言

这就是C++11(二)。大家自己好好消化!今天就分享到这!感谢各位的耐心垂阅!咱们下期见!拜拜~


http://www.kler.cn/a/552621.html

相关文章:

  • GIT提错分支,回滚提交
  • SOME/IP--协议英文原文讲解7
  • 蓝桥杯 Java B 组之日期与时间计算(闰年、星期计算)
  • 使用API有效率地管理Dynadot域名,参与过期域名竞价
  • 系统学习算法:专题十一 floodfill算法
  • 无人机避障——配置新NX
  • 出现 [ app.json 文件内容错误] app.json: 在项目根目录未找到 app.json (env: Windows,mp 解决方法
  • C#程序中进行打印输出文本
  • opencascade 源码学习找到edge对应的face BRepBuilderAPI-BRepBuilderAPI_FindPlane
  • 架构师面试(二):计算机编程基础
  • 极限网关核心架构解析:从 Nginx 到 INFINI Gateway 的演进
  • ABB机器人的二次开发
  • Ubuntu 下 nginx-1.24.0 源码分析 - ngx_palloc_block函数
  • golang面试题:两个interface{} 能不能比较?
  • 接口自动化框架篇:Pytest中的接口请求封装!
  • idea日常报错之UTF-8不可映射的字符
  • 游戏引擎学习第108天
  • MySQL数据库(3)—— 表操作
  • 深入解析LVS命令参数及DR模式下的ARP抑制原理
  • MybaitsPlus学习笔记(三)常用注解