区分万能引用和右值引用

本文最后更新于:April 20, 2022 am

本篇为介绍右值引用、移动语义和完美转发系列博客的第二篇,主要对右值引用和万能引用做一个区分

右值引用,移动语义、完美转发系列目录:右值、移动语义和完美转发

T&&的两层含义

俗话说,知道了真相,才会感到自在。但是在一定的条件下,一种精心编造的谎言也会同样地让人感到自在。本篇博文介绍的概念——万能引用——就是那样一个谎言,但考虑到我们正在和软件层面打交道,换一种说法,这其实是一种抽象。

平时,如果需要声明某类型T的右值引用,我们会写T&&,但反过来并不成立——形如T&&的引用并不都是右值引用

1
2
3
4
5
6
7
8
9
void f(Widget && param);			// rvalue reference
Widget&& var1 = Widget(); // rvalue reference
auto&& var2 = var1; // not rvalue reference

template<typename T>void f(std::vector<T>&& param);
// rvalue reference

template<typename T>void f(T&& param);
// not rvalue reference

事实上,T&&有两层意思,一层意思就是寻常意义上的右值引用,它表现得和我们的预期一致——它们只能被绑定到右值上,并且它们的存在的目的就是为了标识那些可以被用于移动的对象。

在另一层意思上,T&&既有可能是左值引用,也有可能是右值引用。虽然这种表示方法在源代码中看起来就是右值引用,但是它们的的确确可以表现得是左值引用(像T&一样)。T&&的天然双重意义让它们既可以被绑定到右值也可以被绑定到左值。

不止于此,它们还可以被绑定到const 或者non-const的对象上,亦或是volatile或者non-volatile的对象上,又或者是constvolatile的对象上。 总之,它们可以被绑定到任何东西上。

拥有如此前所未有的灵活性的引用值得拥有它们自己的名字——Scott Meyers将之称为万能引用(Universal Reference)

出现场景

万能引用出现在两种场景里,最普遍的一个场景是在函数模板的类型参数里,比如

1
template <typename T> void f(T&& param);

第二种场景上面也提到过了,就是用auto&&来声明的引用

1
auto&& var2= var1;

这两种情况的共同之处在于,它们都涉及了类型推导。在函数模板f中,param的类型是被推导出来的;在声明var2时,var2的类型也是被推导出来的。

将上述两个例子同下面的代码对比,当我们看到T&&,并且不包含类型推导时,可以肯定我们在看的东西是右值引用:

1
2
3
4
5
void f(Widget&& param);		// no type redunction
// param is a rvalue reference

Widget&& var1= Widget(); // no type redunction
// var1 is a rvalue reference

由于万能引用也是一种引用,它们也必须被初始化。用于初始化的值就决定了它究竟代表一个右值引用还是左值引用。如果用一个右值去初始化它,那么没问题,万能引用就是右值引用;如果用一个左值去初始化它,那它就代表了一个左值引用。在万能引用被应用于函数模板的情况下,初始化的值在主调函数中提供:

1
2
3
4
5
6
7
8
9
tempalte<typename T> void f(T&& param);
// param is a universal reference

Widget w;
f(w);
// passing lvalue to f; param's type is Widget&

f(std::move(w));
// passing rvalue to f; param's type is Widget&&

上文提到,万能引用出现的上下文中都有类型推导,但是反过来并不成立,即有类型推导不代表会有万能引用——必须精确地用T&&的形式,其它方式都不行。回看这段代码:

1
2
template<typename T>void f(std::vector<T>&& param);
// param is a rvalue reference

f被调用时,T的具体类型会被推导(除非调用者显式指明类型参数,但在此处不做讨论),但是param的声明形式并非T&&,而是std::vector<T>&&

相应地,param就不可以是一个万能引用,它只能是一个右值引用——当往函数里扔左值时,会报错

1
2
std::vector<int> v;
f(v); //error! 不能用左值初始化右值引用

甚至一个简单的const修饰符都可以让得一个引用丧失成为万能引用的资格:

1
2
template<typename T> void f(const T&& param);
// param is a rvalue reference

如果我们在一个模板类中看到了一个函数的参数是T&&,千万别以为它也是个万能引用,在模板里也不能保证会有类型推导——考虑std::vectorpush_back成员函数

1
2
3
4
5
6
template<class T, class Allocator = allocator<T>>
class vector{
public:
void push_back(T&& x);
...
}

push_back函数的参数的确是T&&的形式,但是很遗憾,这里没有类型推导。因为push_back不能单独脱离某个实例化的vector类而独立存在,也就是说,在创建vector对象时,类型参数就已经确定了,此处的T&&会被换成具体的类型的右值引用。比如,用Widget作为vector的类型参数,vector<Widget>就会被实例化成这样。

1
2
3
4
5
class vector<Widget, allocator<Widget>>{
public:
void push_back(Widget&& x);// rvalue reference
...
};

我们可能会想当然地认为另一个相似的成员函数,emplace_back也会干同样的事情,即它的参数也是右值引用。这并不正确,因为emplace_back的的确确存在类型推导:

1
2
3
4
5
6
7
template<class T, class Allocator = allocator<T>>
class vector{
public:
template<class... Args>
void emplace_back(Args&&... args);
...
};

此处类型参数ArgsT是没有关系的,所以每一次调用emplace_back都会进行一次类型推导——其实更准确来说,这里的Args是一个参数包(parameter pack),但是为了讨论方便,此处将之视为普通的类型参数即可

emplace_back的类型参数名为Args,它还是一个万能引用,重复强调了一点——万能引用必须为T&&的形式,当然,T可以换成其它表示方法,比如type

1
2
template<typename MyTemplateType>
void someFunc(MyTemplateType&& param); //still universal reference

早前提到的auto声明的引用也可能是万能引用,其实其形式也是固定的——仅限于auto&&。这样的万能引用在函数模板的类型参数中并不常见,但在C++11中不时出现;在C++14中,它们更频繁地现身,因为C++14的lamda可能要声明auto&&类型的参数。举例来说,我们可能想要写一个C++14的lamda,用来记录调用某一个函数所花的时间,可以这么写:

1
2
3
4
5
6
7
auto timeFunctionInvocation=[](auto&& func, auto&&... params){
start timer;
std::forward<decltype(func)>(func)(
std::forward<decltype(params)>(params)...
);
stop timer and record time;
};

如果我们暂时无法理解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 协议 ,转载请注明出处!