C++笔记-C++11(二)

发布于:2025-06-12 ⋅ 阅读:(14) ⋅ 点赞:(0)

紧接上文,我们在介绍了移动构造和移动赋值时仅仅只介绍了函数返回值的情况,那么什么场景还会用到呢?我们接着往下看:

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

通过查看STL文档中的一些容器如vector,list,我们可以看出在他们中push和insert系列接口在C++11中引入了右值引用的版本。

这些信息就体现在我们现在要讲的在传参上的体现,同时也告诉我们当参数是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象;当对象是一个右值时,容器内部则调用移动构造,右值对象的资源到容器空间的对象上。

我们看这几个例子,在传参时也同样调用了相应的拷贝构造和移动构造。

上面我们是利用了stl库中的list来控制我们自己写的string类,下面我们来尝试写出list中的push_back和insert中对右值引用的相关操作:

下面以我们之前在list的章节自己实现list的为例,这里就不展示全部的代码了。

这是我们对之前写的list所进行的修改,这里注意我们在参数部分虽然用了右值引用,但是上节我们讲了变量表达式均为左值,所以我们在复用传参时还要注意将变量表达式通过move函数强转为右值,不然还是会调用左值的拷贝构造函数,并且少一个都不行。

这是没有问题的list,和我们调用库中的一样,但是:

这里我就把ListNode中的构造函数少写了move函数来将参数强转为右值,通过上图可以看出结果发生了改变,只调用了左值的拷贝构造函数,所以我们在自己联系时一定要注意这点。

3.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),泛左值包含将亡值和左值。
用图来展示它们之间的关系就如上图所示,C++将其这样分类意义其实不大,所以这个知识点大家了解一下即可,知道有这么个名词即可。
3.7引用折叠
上一节我们讲了右值引用后有人可能想过要是左值引用和右值引用叠加在一起到底是左值引用呢,还是右值引用呢?
下面我们就来解决这个问题:
首先 C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef
中的类型操作可以构成引⽤的引⽤。
直接这样叠加写是不允许的,代码会直接报错,所以我们要通过模板或typedef来操作,并且左值引用和右值引用叠加使用要遵循以下原则:
右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。这是C++规定的,所以说大家也不必纠结为什么这样写。
我们来看上面的几个例子,答案我也写在每行代码的后面,就是根据上面的原则来判断此时的引用是左值引用还是右值引用。
讲了typedef的情景,我们再来看看函数模板的情景:
 
这里我们就定义一个左值引用的函数模板,可以看出有的调用会报错,这里报错的原因相信大家到这都已经知道,并且我也将每种例子经过折叠后的引用类型以注释的形式写了出来,大家看上图即可。
在这种形式下,如果参数是左值引用,那么不管你传的是什么类型,它只会是左值引用,这个结论我们通过上图的各种示例可以得出。
那么如果参数是右值引用呢?
这里我就写了一个参数为右值引用的函数模板,依旧有一些例子也会报错,大家主要看我注释写的经过折叠后变为了什么类型的引用。
经过砂锅面的一些例子我们可以得出,如果参数模板是右值引用,那么经过折叠可能为左值引用,也可能为右值引用,这种引用我们称为“万能引用”。
那么我们回过头来看上面的list代码:
大家思考一下,这是“万能引用”吗?
可能有人会觉得是,也有人觉得不是,第一次接触的话估计都会选错。
答案不是,这点要和函数模板区分开,函数模板是在传参时通过推导确定了类型,而类模板才显式实例化时就已经了确定了类型,换言之,如果我们在显式实例化时传的类型是int,那么T就为int,不会为其他类型,而函数模板不一样,你传什么类型,T就是什么类型,大家要弄清楚它们之间的区别。
而如何将类模板中的类似这样的函数改成“万能引用”呢?我们接着往下看:
3.8完美转发
我们来看这几个例子,相关的注释我已经写在代码旁,这里有人会疑惑:为什么传的参数是左值,推导出来的类型却是左值引用,而传右值,就是类型了?
这点我在上面引用折叠部分没有讲,这里解释一下,这里的推导时C++规定的,它就是这样推导的,大家记着即可,你拿上面的例子也是如此,不过仅限于参数是右值引用,因为参数是左值引用你不管传什么类型都是左值引用,所以这里我只拿右值引用举例,而上面的右值引用例子之所以有的会报错,是因为我们显式实例化了,这里我并没有显式实例化,这里大家不要弄混了。
这里没有用显式实例化是为了讲完美转变。
了解完这个我们来思考一下,这些函数调用的结果是什么?
这里大家估计都能猜对,没错,全部都是相关的左值引用,那么我们现在面临一个问题:我传的是右值,我想调用右值引用该怎么办?
我想肯定有人会这样做,加个move函数不就好了,那我问你:那我传左值怎么办?
这种做法就会导致传左值却调不到左值引用的函数,所以该如何解决这个问题呢?
这里就要引入完美转变forward函数:
完美转发forward本质是⼀个函数模板,它主要还是通过引⽤折叠的⽅式实现,上⾯⽰例中传递给
Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给
Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤
返回。
这样我们就能传左值调用左值引用,传右值调用右值引用:
如上图所示,我们在使用forward函数后,结果就如我们所愿。
回到上面引用折叠最后的问题,讲过这里就能解决:
想变成“万能引用”,就可以将它们写成函数模板,结果和我们之前分开写是一样的:
 
依旧是调用相应的的拷贝构造和移动构造,这里可能有人会问:那上面的ListNode中的构造函数可以这样写吗?
答案可以,但不建议,因为如果写成这种形式,构造函数就不是默认构造函数了,就会出现别的问题,因为写成这样就是让你传参,如果不传参就会报错,所以并不建议这样写。
4.可变参数模板
4.1基本语法及原理
C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称
为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函
数参数。
基本格式就如上图所示, 我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出 接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板 ⼀样,每个参数实例化时遵循引⽤折叠规则。
下面我们来看一些例子:
这里就利用可变参数模板写了一个函数,而里面的sizeof...函数是一种特定的函数,是专门来计算可变参数模板中有几个参数,并且可变参数模板是可以传0个参数的。
可变参数模板的原理和模板类似,本质还是去实例化对应类型和个数的多个函数:
 
就如上图所示,就是实例化成这几个函数,其实可变参数模板就相当于模板的泛型,我们之前如果要想传不同个数的参数就要写不同参数个数的函数模板,而可变参数模板一个函数就能解决。
4.2包扩展
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个
包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元
素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。
下面我们来看一个包扩展的例子:
正如我们上面所说,在包扩展的同时我们还要提供 每个扩展元素的模式,这里我们就简单一点,只打印出相应的数据即可。
本质上编译器将可变参数模板通过模式的包扩展,编译器推导了上面的三个重载函数。
看了上面的例子,有人可能会有疑惑:当参数为0时我能不能这样写:
答案是不可以的,会报上面的错误,因为这里ShowList的递归是在编译时就完成的,而这个判断条件是在运行时才看,所以在编译时没有找到相应的无参数的ShowLIst函数,就会报上面的错误。
包扩展这点了解一下即可,这个语法也就在一些特殊情况下才会使用,一般用不到。
4.3emplace系列接口
C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上
兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container<T>,empalce还⽀持
直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
以list容器为例,这就是STL库中的emplace系列接口,可以看出他们的均为可变参数模板,只要容器有push或者insert的接口,就会有相应的emplace接口。
下面我们来看一些例子:
从上面的结果我们可以看出,除了最后一种情况外,emplace_back接口和push_back接口调用的函数是一样的,左值就调用拷贝构造,右值就调用移动构造,但是在最后一种情况中就发生了变化。
当我们传构造T对象的参数时,emplace_back接口只调用了构造函数,并没有和push_back一样去调用移动构造,为什么呢?
我们知道,push_back函数在类中并不是函数模板,所以在容器显式实例化时,它的参数类型就已经确定了,所以在调用时,它就是一个普通函数;而emplace_back函数在类中依旧是函数模板,所以在传参时才会去推导参数的类型。
而在上面的例子中,前面两个传参后emplace_back推导的类型皆为string,它有资源可以转移,所以才会去调用相应的拷贝构造和移动构造,而第三种情况它推导的是const char*,就是一个常量字符串,它没有资源需要转移,所以会在容器中直接构造一个对象。
也因为这个原因,在一些情况下emplace系列的接口效率要更高一些,但是说实话也没高多少,毕竟也就少了个移动构造和析构,这两个效率也都不低,但确实要高一些。
这里我们只穿了一个参数,说实话并没有体现出可变参数模板,我们再来看一个例子:
这次我们来传一个pair,要注意第三种情况的传参方式,有人可能会这样写:
这样写是不行的 push_back可以这样传也是因为在容器显式实例化时就已经确定参数类型是pair,所以这样传没问题,而emplace_back在传参时再去推导,{}编译器会将其自动识别为Initializer_list,而Initializer_list参数类型是一样的,这里参数类型不同,所以推不出来。
可能有人会问:那这样传参,怎么保证能够构造成功呢?
编译器会拿着参数包去和pair中的类型去匹配,如果匹配成功就会构造一个pair,如果匹配不成功,就会报错:
如上图所示,这里参数就不匹配了,那么就会报错。
说了这么多,我们接下来来自己实现一下emplace系列的接口:
这里我写成完美引用的方式,所以就要解决相应的问题,需要用完美转变。并且要注意...三个点要写在括号外,不要写在括号里面了。
下面我们用自己写的list来检验是否和库里面是一样的效果:
 
可以看出我们自己实现的list同样能够完成相应的操作,最后一种情况也是只调用了构造函数,没有调用移动构造。
以上就是C++11(二)的内容。