也许C ++ 11中最重要的新功能是右值引用; 它们是构建移动语义和完美转发的基础。(如果您不熟悉右值引用的基础知识,移动语义或完美转发,您可能希望在继续之前阅读Thomas Becker的概述。)
在语法上,rvalue引用被声明为“普通”引用(现在称为右值引用),除了使用两个&符号而不是一个。此函数采用rvalue-reference-to-类型的参数Widget
:
void f (Widget && param );
鉴于使用“ &&
” &&
声明了右值引用,假设类型声明中存在“ ”表示右值引用似乎是合理的。事实并非如此:
Widget && var1 = someWidget ; //这里,“&&”表示右值参考
auto && var2 = var1 ; //这里,“&&”并不能意味着右值引用
template < typename T >
void f (std :: vector < T > && param ); //这里,“&&”表示右值参考
template < typename T >
void f (T && param ); //这里,“&&”并不能意味着右值引用
在本文中,我将&&
在类型声明中描述“ ” 的两个含义,解释如何区分它们,并引入新术语,使得可以明确地传达“ &&
”的含义。区分不同的含义很重要,因为如果你&&
在类型声明中看到“ ” 时想到“右值引用” ,你就会误读很多C ++ 11代码。
这个问题的实质是,“ &&
”在类型声明有时意味着右值引用,但有时这意味着无论是右值引用或左值参考。因此,&&
源代码中某些出现的“ ”实际上可能具有“ &
” 的含义,即具有右值引用(“ ”)的句法外观&&
,但是左值引用的含义(“&
“)。可能的参考比左值参考或右值参考更灵活。例如,Rvalue引用只能绑定到rvalues,而且左值引用除了能够绑定到左值外,还可以仅在受限情况下绑定到rvalues。[1] 相反,用“ &&
” 声明的引用可以是左值引用或右值引用可以绑定到任何东西。这些异常灵活的引用值得拥有自己的名字。我称之为普遍参考。
“ &&
”表示通用引用的详细信息(即,&&
源代码中的“ &
” 实际上可能意味着“ ”)是棘手的,所以我将推迟细节的覆盖直到稍后。现在,让我们关注以下经验法则,因为这是您在日常编程中需要记住的:
如果声明变量或参数具有
T&&
某种推导类型的类型T
,则该变量或参数是通用引用。
涉及类型推导的要求限制了可以找到通用引用的情况。实际上,几乎所有通用引用都是函数模板的参数。因为auto
-declared变量的类型推导规则与模板基本相同,所以也可以使用auto
-declared通用引用。这些在生产代码中并不常见,但我在本文中展示了一些内容,因为它们在示例中比模板更简洁。在本文的Nitty Gritty详细信息部分中,我解释说,通用引用也可能与使用typedef
和decltype
,但在我们深入了解细节之前,我将继续进行,就好像通用引用只涉及函数模板参数和auto
-declared变量一样。
通用引用形式的约束T&&
比它看起来更重要,但我会推迟检查,直到稍后。现在,请简单地记下要求。
与所有引用一样,必须初始化通用引用,并且它是通用引用的初始值设定项,用于确定它是表示左值引用还是右值引用:
- 如果初始化通用引用的表达式是左值,则通用引用将成为左值引用。
- 如果初始化通用引用的表达式是rvalue,则通用引用将成为右值引用。
仅当您能够区分左值和右值时,此信息才有用。这些术语的精确定义很难开发(C ++ 11标准通常根据具体情况指定表达式是左值还是右值),但实际上,以下内容足够:
- 如果可以获取表达式的地址,则表达式为左值。
- 如果表达式的类型是左值引用(例如,
T&
或const T&
等),则该表达式是左值。 - 否则,表达式是右值。从概念上(通常也是实际上),rvalues对应于临时对象,例如从函数返回的或通过隐式类型转换创建的对象。大多数文字值(例如,
10
和5.3
)也是右值。
再次考虑本文开头的以下代码:
Widget && var1 = someWidget ;
auto && var2 = var1 ;
你可以拿地址var1
,左右var1
也是。 var2
的类型声明auto&&
使它成为通用引用,并且因为它是用var1
(左值)初始化的,所以它var2
变成了左值引用。随意阅读源代码可能会让您相信这var2
是一个右值引用; &&
其声明中的“ ”肯定表明了这一结论。但因为它是一个用左值初始化的通用引用,所以var2
成为左值引用。这是因为如果var2
被声明如下:
Widget &var2 = var1 ;
如上所述,如果表达式具有类型左值引用,则它是左值。考虑这个例子:
std :: vector <int> v ;
...
auto && val = v [ 0 ]; // val成为左值参考(见下文)
val
是一个通用引用,它正在被初始化v[0]
,即通过调用的结果std::vector<int>::operator[]
。该函数返回对向量元素的左值引用。[2] 因为所有左值引用都是左值,并且因为这个左值用于初始化val
,所以val
变成左值引用,即使它被声明为右值引用的声明。
我注意到通用引用最常见的是模板函数中的参数。从本文开头再次考虑这个模板:
template < typename T >
void f (T && param ); //“&&” 可能表示右值参考
鉴于这个f的调用,
f (10 ); // 10是一个右值
param
用文字初始化10
,因为你不能取其地址,是一个右值。这意味着在调用中f
,通用引用param
用rvalue初始化,因此param
变为rvalue引用 - 特别是int&&
。
另一方面,如果f
像这样调用,
int x = 10 ;
f (x ); // x是左值
param
用变量初始化x
,因为你可以取其地址,是一个左值。这意味着在这个调用中f
,通用引用参数用左值初始化,因此param成为左值引用 - int&
确切地说。
声明旁边的注释f
现在应该是明确的:param
类型是左值引用还是右值引用取决于f
调用时传递的内容。有时param成为左值引用,有时它变成右值引用。 param
真的是一个普遍的参考。
请记住,“ &&
”表示仅在进行类型推导的情况下的通用引用。没有类型扣除的地方,没有普遍的参考。在这种情况下,&&
类型声明中的“ ”总是表示右值引用。因此:
template<typename T>
void f(T&& param); // deduced parameter type ⇒ type deduction;
// && ≡ universal reference
template<typename T>
class Widget {
...
Widget(Widget&& rhs); // fully specified parameter type ⇒ no type deduction;
... // && ≡ rvalue reference
};
template<typename T1>
class Gadget {
...
template<typename T2>
Gadget(T2&& rhs); // deduced parameter type ⇒ type deduction;
... // && ≡ universal reference
};
void f(Widget&& param); // fully specified parameter type ⇒ no type deduction;
// && ≡ rvalue reference
这些例子并不令人惊讶。在每种情况下,如果您看到T&&
(T
模板参数在哪里),都有类型推导,因此您正在查看通用引用。如果您&&
在特定类型名称(例如Widget&&
)之后看到“ ” ,则表示您正在查看右值引用。
我说参考声明的形式必须是“ T&&
”才能使参考文献具有普遍性。这是一个重要的警告。再看一下本文开头的这个声明:
template < typename T >
void f (std :: vector < T > && param ); //“&&”表示右值参考
这里,我们有类型推导和&&
“ - 声明函数”参数,但参数声明的形式不是“ T&&
”,它是“ std::vector<t>&&
”。因此,该参数是正常的右值参考,而不是通用参考。通用引用只能以“ T&&
” 形式出现!即使简单地添加const
限定符也足以禁用“ &&
”作为通用引用的解释:
template < typename T >
void f (const T && param ); //“&&”表示右值参考
现在,“ T&&
”只是通用参考的必需形式。这并不意味着您必须使用T
模板参数的名称:
template < typename MyTemplateParamType >
void f (MyTemplateParamType && param ); //“&&”表示通用参考
有时您可以T&&
在函数模板声明中看到哪里T
是模板参数,但仍然没有类型推导。考虑这个push_back
功能std::vector
:[3]
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
void push_back(T&& x); // fully specified parameter type ⇒ no type deduction;
... // && ≡ rvalue reference
};
这里,T
是一个模板参数,并push_back
取一个T&&
,但参数不是通用引用!怎么可能?
如果我们看看如何push_back
在课堂外宣布,答案就会变得明显。我要假装std::vector
的Allocator
参数不存在,因为它是与本讨论无关,它只是杂波了代码。考虑到这一点,这是这个版本的声明std::vector::push_back
:
template <class T>
void vector<T>::push_back(T&& x);
push_back
没有std::vector<T>
包含它的类就不能存在。但是,如果我们有一个班级std::vector<T>
,我们已经知道是什么T
,所以没有必要去推断它。
一个例子会有所帮助。如果我写
Widget makeWidget(); // factory function for Widget
std::vector<Widget> vw;
...
Widget w;
vw.push_back(makeWidget()); // create Widget from factory, add it to vw
我的使用push_back
将导致编译器为类实例化该函数std::vector<Widget>
。声明push_back
如下:
void std :: vector < Widget > :: push_back (Widget && x );
看到?一旦我们知道了类std::vector<Widget>
,那么push_back
参数的类型就完全确定了:它就是Widget&&
。这里没有类型扣除的作用。
与std::vector
's 相对照,emplace_back
声明如下:
template <class T, class Allocator = allocator<T> >
class vector {
public:
...
template <class... Args>
void emplace_back(Args&&... args); // deduced parameter types ⇒ type deduction;
... // && ≡ universal references
};
不要让这样一个事实emplace_back
采用可变数量的参数(如由椭圆中的声明表示Args
和args
)由一个事实,即一类为每个这样的参数必须推断让你分心。函数模板参数Args
独立于类模板参数T
,因此即使我们知道该类是,std::vector<Widget>
也不会告诉我们所采用的类型emplace_back
。外的类声明emplace_back
为std::vector<Widget>
使得清楚了(我继续忽略的存在Allocator
参数):
template<class... Args>
void std::vector<Widget>::emplace_back(Args&&... args);
显然,知道该类std::vector<Widget>
并不能消除编译器推断传递类型的必要性emplace_back
。因此,std::vector::emplace_back
参数是通用引用,不同于std::vector::push_back
我们检查的版本的参数,它是一个右值引用。
最后一点值得记住:表达式的左值或右值与其类型无关。考虑类型int
。存在类型的左int
值(例如,声明为int
s的变量),并且存在类型的右值int
(例如,像文字一样10
)。对于用户定义的类型,它是相同的Widget
。甲Widget
对象可以是一个左值(例如,Widget
变量)或右值(例如,对象从一个返回Widget
-creating工厂功能)。表达式的类型不会告诉您它是左值还是右值。
因为表达式的左值或右值与其类型无关,所以可以使用其值类型为右值引用的左值,也可以使用rvalue类型参考的rvalues:
Widget makeWidget(); // factory function for Widget
Widget&& var1 = makeWidget() // var1 is an lvalue, but
// its type is rvalue reference (to Widget)
Widget var2 = static_cast<Widget&&>(var1); // the cast expression yields an rvalue, but
// its type is rvalue reference (to Widget)
将var1
左值(例如)转换为右值的常规方法是使用std::move
它们,因此var2
可以这样定义:
Widget makeWidget(); // factory function for Widget
Widget&& var1 = makeWidget() // var1 is an lvalue, but
// its type is rvalue reference (to Widget)
Widget var2 = static_cast<Widget&&>(var1); // the cast expression yields an rvalue, but
// its type is rvalue reference (to Widget)
我最初展示的代码static_cast
只是为了明确表达式的类型是一个右值引用(Widget&&
)。
rvalue引用类型的命名变量和参数是左值。(您可以使用他们的地址。)再次考虑前面的Widget
和Gadget
模板:
template<typename T>
class Widget {
...
Widget(Widget&& rhs); // rhs’s type is rvalue reference,
... // but rhs itself is an lvalue
};
template<typename T1>
class Gadget {
...
template <typename T2>
Gadget(T2&& rhs); // rhs is a universal reference whose type will
... // eventually become an rvalue reference or
}; // an lvalue reference, but rhs itself is an lvalue
在Widget
构造函数中,rhs
是一个右值引用,所以我们知道它绑定了一个rvalue(即rvalue被传递给它),但rhs
它本身是一个左值,所以如果我们想采取它,我们必须将它转换回rvalue它所受约束的价值的优势。我们的动机通常是将它用作移动操作的源,这就是为什么要使用左值转换为右值的方法std::move
。类似地,rhs
在Gadget
构造函数中是一个通用引用,因此它可能绑定到左值或右值,但不管它绑定到什么,它rhs
本身都是左值。如果它绑定到右值并且我们想要利用它所绑定的rvalueness,我们必须转换rhs
回到右边。当然,如果它与左值绑定,我们不希望将其视为左值。关于通用引用绑定的std::forward
左值和右值的这种模糊性是这样的动机:获取通用引用左值并仅在它绑定的表达式是右值时将其转换为右值。函数的名称(“ forward
”)是一种确认,我们执行这种转换的愿望实际上总是在传递 - 转发 - 它到另一个函数时保留调用参数的左值或右值。
但是,std::move
和std::forward
不是本文的重点。&&
类型声明中的“ ”可能会或可能不会声明右值引用。为避免稀释该焦点,我将向您推荐“ 更多信息”部分中的参考资料,以获取有关std::move
和的信息std::forward
。
Nitty Gritty细节
问题的真正核心是C ++ 11中的一些构造引用了对引用的引用,并且在C ++中不允许引用引用。如果源代码显式包含对引用的引用,则代码无效:
Widget w1;
...
Widget& & w2 = w1; // error! No such thing as “reference to reference”
但是,有些情况下,由于在编译期间发生的类型操作而引起对引用的引用,并且在这种情况下,拒绝代码将是有问题的。我们从C ++初始标准的经验中了解到这一点,即C ++ 98 / C ++ 03。
在作为通用引用的模板参数的类型推导期间,推导出相同类型的左值和右值具有稍微不同的类型。特别T
地,类型的T&
左值被推断为类型(即,左值引用T
),而类型的右值T
被推断为简单类型T
。(注意,虽然lvalues被推导为左值引用,但rvalues不会推导为rvalue引用!)考虑当使用rvalue和lvalue调用采用通用引用的模板函数时会发生什么:
template<typename T>
void f(T&& param);
...
int x;
...
f(10); // invoke f on rvalue
f(x); // invoke f on lvalue
在f
使用rvalue 的调用中10
,T
推断为int
,并且实例化f
如下所示:
void f (int && param ); // f从rvalue实例化
没关系。然而,f
在使用左值的调用x
中,T
推断为int&
,并且f
实例化包含对引用的引用:
void f (int && param ); // f从rvalue实例化
由于引用引用,这个实例化的代码初步无效,但源代码 - “ f(x)
” - 是完全合理的。为避免拒绝它,C ++ 11在对模板实例化等上下文中引用引用时执行“引用折叠”。
因为有两种引用(左值引用和右值引用),所以有四种可能的引用引用组合:对左值引用的左值引用,对右值引用的左值引用,对左值引用的右值引用,以及对右值引用的右值引用。只有两个参考折叠规则:
- 对右值引用的右值引用变为(“折叠成”)右值引用。
- 对引用的所有其他引用(即涉及左值引用的所有组合)都会折叠为左值引用。
将这些规则应用于左值上f的实例化会产生以下有效代码,这是编译器处理调用的方式:
void f (int& param ); //在引用折叠后使用左值实例化f
这证明了在类型推导和参考折叠之后,通用引用可以成为左值引用的精确机制。事实是,通用引用实际上只是参考折叠上下文中的右值引用。
在推导出一个本身就是引用的变量的类型时,事情会变得微妙。在这种情况下,将忽略该类型的引用部分。例如,给定
int x ;
...
int && r1 = 10 ; // r1的类型是int &&
int & r2 = x ; // r2的类型是int&
对于这两种类型r1
和r2
被认为是int
在对模板的调用f
。这种引用剥离行为独立于以下规则:在通用引用的类型推导期间,左值被推断为类型T&
和类型的rvalues T
,因此给定这些调用,
f(r1);
f(r2);
推导的类型都r1
和r2
是int&
。为什么?首先剥离r1
's和r2
's类型的引用部分(int
在两种情况下都会产生),然后,因为每个都是左值,每个都被视为int&
在调用中的通用引用参数的类型推导期间f
。
正如我所指出的,参考折叠发生在“模板实例化等上下文”中。第二个这样的上下文是auto
变量的定义。对于auto
作为通用引用的变量的类型推导与作为通用引用的函数模板参数的类型推导基本相同,因此T
推导出类型的T&
左值具有类型,并且推导出类型的rvalues T
具有类型T
。从本文开头再次考虑这个例子:
Widget && var1 = someWidget ; // var1的类型为Widget &&(此处不使用auto)
auto && var2 = var1 ; // var2的类型为Widget&(见下文)
var1
是类型的Widget&&
,但在初始化中的类型推导期间忽略了它的引用var2
; 它被认为是类型Widget
。因为它是用于初始化通用引用(var2
)的左值,所以它的推导类型是Widget&
。替换Widget&
为auto
在定义的var2
收益率以下无效代码,
Widget& && var2 = var1; // note reference-to-reference
在参考坍塌之后,它变成了
Widget& var2 = var1; // var2 is of type Widget&
第三个参考折叠上下文是typedef
形成和使用。鉴于此类模板,
template < typename T >
class Widget {
typedef T &LvalueRefType ;
...
};
以及模板的这种用法,
Widget<int&> w;
实例化的类将包含此(无效)typedef:
typedef int& & LvalueRefType;
引用折叠将其减少为此合法代码:
typedef int& LvalueRefType;
如果我们typedef
在应用引用的上下文中使用它,例如,
void f (Widget <int &> :: LvalueRefType && param );
扩展后产生以下无效代码typedef
,
void f(int& && param);
但是参考崩溃了,所以f
最终的声明是这样的:
void f(int& param);
引用折叠发生的最终背景是使用decltype。
与模板和auto的情况一样,decltype对产生T或T&类型的表达式执行类型推导,然后decltype应用C ++ 11的引用 - 折叠规则。
唉,decltype使用的类型推导规则与模板或自动类型推导中使用的规则不同。
详细信息对于这里的覆盖范围来说太神秘了(更多信息部分提供指向,呃,更多信息),但值得注意的是,给定非引用类型的命名变量的decltype推导出类型T(即,非 -reference类型),在相同条件下,模板和自动推断类型T&。
另一个重要的区别是decltype的类型推导仅取决于decltype表达式; 忽略初始化表达式的类型(如果有)。
人机工程学:
Widget w1, w2;
auto&& v1 = w1; // v1 is an auto-based universal reference being
// initialized with an lvalue, so v1 becomes an
// lvalue reference referring to w1.
decltype(w1)&& v2 = w2; // v2 is a decltype-based universal reference, and
// decltype(w1) is Widget, so v2 becomes an rvalue reference.
// w2 is an lvalue, and it’s not legal to initialize an
// rvalue reference with an lvalue, so
// this code does not compile.
摘要
在类型声明中,“ &&
”表示右值引用或通用引用 - 可以解析为左值引用或右值引用的引用。通用引用总是具有T&&
某种推断类型的形式T
。
参考折叠是导致通用引用的机制(实际上只是在引用折叠发生的情况下的右值引用)有时会解析为左值引用,有时会转换为右值引用。它出现在指定的上下文中,在编译期间可能会引用对引用的引用。这些上下文是模板类型推导,auto
类型推导,typedef
形成和使用以及decltype
表达式。
致谢
本文的草案版本由Cassio Neri,Michal Mocny,Howard Hinnant,Andrei Alexandrescu,Stephan T. Lavavej,Roger Orr,Chris Oldwood,Jonathan Wakely和Anthony Williams审阅。他们的评论有助于大幅改进文章内容及其陈述。
笔记
[1]我将在本文后面讨论rvalues及其对应物,左值。对左值引用绑定到rvalues的限制是只有当左值引用被声明为引用时才允许这样的绑定const
,即a const T&
。
[2]我忽略了违规行为的可能性。它们产生不确定的行为。
[3] std::vector::push_back
超载。显示的版本是本文中唯一感兴趣的版本。
更多的信息
C ++ 11,维基百科。
新C ++概述(C ++ 11),Scott Meyers,Artima Press,最后更新于2012年1月。
C ++ Rvalue References Explained,Thomas Becker,最后更新于2011年9月。
decltype,维基百科。
“关于decltype的注释,” Andrew Koenig,Dobb博士,2011年7月27日。