区分万能引用和右值引用
本文最后更新于:April 20, 2022 am
本篇为介绍右值引用、移动语义和完美转发系列博客的第二篇,主要对右值引用和万能引用做一个区分
右值引用,移动语义、完美转发系列目录:右值、移动语义和完美转发
T&&的两层含义
俗话说,知道了真相,才会感到自在。但是在一定的条件下,一种精心编造的谎言也会同样地让人感到自在。本篇博文介绍的概念——万能引用——就是那样一个谎言,但考虑到我们正在和软件层面打交道,换一种说法,这其实是一种抽象。
平时,如果需要声明某类型T
的右值引用,我们会写T&&
,但反过来并不成立——形如T&&
的引用并不都是右值引用
1 |
|
事实上,T&&
有两层意思,一层意思就是寻常意义上的右值引用,它表现得和我们的预期一致——它们只能被绑定到右值上,并且它们的存在的目的就是为了标识那些可以被用于移动的对象。
在另一层意思上,T&&
既有可能是左值引用,也有可能是右值引用。虽然这种表示方法在源代码中看起来就是右值引用,但是它们的的确确可以表现得是左值引用(像T&
一样)。T&&
的天然双重意义让它们既可以被绑定到右值也可以被绑定到左值。
不止于此,它们还可以被绑定到const
或者non-const
的对象上,亦或是volatile
或者non-volatile
的对象上,又或者是const
且 volatile
的对象上。 总之,它们可以被绑定到任何东西上。
拥有如此前所未有的灵活性的引用值得拥有它们自己的名字——Scott Meyers将之称为万能引用(Universal Reference)
出现场景
万能引用出现在两种场景里,最普遍的一个场景是在函数模板的类型参数里,比如
1 |
|
第二种场景上面也提到过了,就是用auto&&来声明的引用
1 |
|
这两种情况的共同之处在于,它们都涉及了类型推导。在函数模板f
中,param
的类型是被推导出来的;在声明var2
时,var2
的类型也是被推导出来的。
将上述两个例子同下面的代码对比,当我们看到T&&
,并且不包含类型推导时,可以肯定我们在看的东西是右值引用:
1 |
|
由于万能引用也是一种引用,它们也必须被初始化。用于初始化的值就决定了它究竟代表一个右值引用还是左值引用。如果用一个右值去初始化它,那么没问题,万能引用就是右值引用;如果用一个左值去初始化它,那它就代表了一个左值引用。在万能引用被应用于函数模板的情况下,初始化的值在主调函数中提供:
1 |
|
上文提到,万能引用出现的上下文中都有类型推导,但是反过来并不成立,即有类型推导不代表会有万能引用——必须精确地用T&&的形式,其它方式都不行。回看这段代码:
1 |
|
当f
被调用时,T
的具体类型会被推导(除非调用者显式指明类型参数,但在此处不做讨论),但是param
的声明形式并非T&&
,而是std::vector<T>&&
相应地,param
就不可以是一个万能引用,它只能是一个右值引用——当往函数里扔左值时,会报错
1 |
|
甚至一个简单的const
修饰符都可以让得一个引用丧失成为万能引用的资格:
1 |
|
如果我们在一个模板类中看到了一个函数的参数是T&&
,千万别以为它也是个万能引用,在模板里也不能保证会有类型推导——考虑std::vector
的push_back
成员函数
1 |
|
push_back
函数的参数的确是T&&
的形式,但是很遗憾,这里没有类型推导。因为push_back
不能单独脱离某个实例化的vector
类而独立存在,也就是说,在创建vector
对象时,类型参数就已经确定了,此处的T&&
会被换成具体的类型的右值引用。比如,用Widget
作为vector
的类型参数,vector<Widget>
就会被实例化成这样。
1 |
|
我们可能会想当然地认为另一个相似的成员函数,emplace_back
也会干同样的事情,即它的参数也是右值引用。这并不正确,因为emplace_back
的的确确存在类型推导:
1 |
|
此处类型参数Args
和T
是没有关系的,所以每一次调用emplace_back
都会进行一次类型推导——其实更准确来说,这里的Args
是一个参数包(parameter pack),但是为了讨论方便,此处将之视为普通的类型参数即可
emplace_back
的类型参数名为Args
,它还是一个万能引用,重复强调了一点——万能引用必须为T&&
的形式,当然,T
可以换成其它表示方法,比如type
等
1 |
|
早前提到的auto
声明的引用也可能是万能引用,其实其形式也是固定的——仅限于auto&&
。这样的万能引用在函数模板的类型参数中并不常见,但在C++11中不时出现;在C++14中,它们更频繁地现身,因为C++14的lamda可能要声明auto&&
类型的参数。举例来说,我们可能想要写一个C++14的lamda,用来记录调用某一个函数所花的时间,可以这么写:
1 |
|
如果我们暂时无法理解std::forward<decltype(blah blah blah)>
,那是因为可能还没有看Item33,不必担心。
func
是一个万能引用,可以被绑定到任何可调用的对象上,不论左值还是右值。args
是≥0个万能引用参数(Universal Reference Parameter Pack),可以被绑定到任意数量个不论左值右值的对象上。多亏了用auto
声明的万能引用,timeFuncInvocation
函数可以记录绝大多数函数的执行时间(至于为什么是绝大多数而不是任何,请参看Item30)
请牢记,本条款——关于万能引用概念的基础——其实是一个谎言,或者说一种抽象,毕竟这货的名字都是Scott取的。底层真正的实现机制其实是叫做引用折叠(reference collapsing, 也是 Item28的讨论内容)。但是多了这一层抽象并不给我们带来任何麻烦,而是更多的方便——它使我们更加准确地理解源代码,也方便了我们和同事之间的交流,同时,它让我们能够更容易地理解接下来的Item25 和Item26。就像在日常生活中,使用经典力学就足够了,而非事事都要考虑相对论效应和量子效应。
总结
如果一个函数模板有形如T&&的参数,并且T具体所指的类型是被推导出来的,或者使用auto&&来声明一个对象,那么这个参数或者对象就是个万能引用
如果声明的方式和T&&的样子不一致,或者没有类型推导发生,那么T&&只会代表右值引用
如果用右值去初始化万能引用,那么万能引用接下来就是一个右值引用;如果用左值去初始化,那么万能引用相应地会相当于一个左值引用。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!