现代C++之万能引用、完美转发、引用折叠
0.导语1.问题引入2.引入万能引用3.万能引用出现场合4.理解左值与右值4.1 精简版4.2 完整版4.3 生命周期延长4.4 生命周期延长应用5.区分万能引用6.表达式的左右值性与类型无关7.引用折叠和完美转发7.1 引用折叠之本质细节7.2 示例与使用7.3 std::move()与std::forward()源码剖析8.不要返回本地变量的引用9.总结10.补充
0.导语
不知道大家有没有听说过Scott Meyers,这位是C++的一位巨佬,知名的《Effective C++》与《Effective Modern C++》都是他编写的,bla bla...
这篇文章是翻译isocpp上的一篇文章以及学习极客时间第三讲的内容、stackoverflow等网站整合起来的,全文1w多字,还望收藏下来慢慢体会。
在谈到万能引用这里,Scott Meyers在本文中详细阐述了所谓的万能引用,同时也在《Effective Modern C++》中详细阐述,大家可以去看看,好了,开始正文。
T&& Doesn’t Always Mean “Rvalue Reference”
by Scott Meyers
文中代码:
https://github.com/Light-City/CPlusPlusThings
1.问题引入
可能右值引用(rvalue references)是C++11里面最重要的新特性了。移动语义和完美转发都建立在它的基础之上。(如果你不熟悉右值引用基础, 移动语义, 或是完美转发, 再继续阅读本文之前你可能需要先看看 Thomas Becker’s overview )。
http://thbecker.net/articles/rvalue_references/p_01.html
从语法上来看,声明右值引用看起来和声明"普通"的引用(现在被称为左值引用(lvalue references))很像,只不过你需要用&&
而不是&
。下面这个函数需要一个类型为rvalue-reference-to-Widget
:的参数:
void f(Widget&& param);
假设右值引用是使用&&
声明的,那么假设类型声明中出现&&
表示右值引用似乎是合理的。事实并非如此:
Widget&& var1 = someWidget; // here, “&&” means rvalue reference
auto&& var2 = var1; // here, “&&” does not mean rvalue reference
template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference
template<typename T>
void f(T&& param); // here, “&&”does not mean rvalue reference
在本文当中,我会对类型声明中 “&&
” 可能具有的两种含义进行阐释,讲解如何区分它们,并且会引入一个新术语以便在交流的时候清楚的表明在当前说的“&&
”是哪种含义。正确的区分这两种含义非常重要,因为如果你看到“&&
”就以为是右值引用的话,你可是会误读很多c++11代码的。
2.引入万能引用
这个问题的本质实际上是,类型声明当中的“&&
”有的时候意味着rvalue reference,但有的时候意味着rvalue reference 或者 lvalue reference。因此,源代码当中出现的 “&&
” 有可能是 “&
” 的意思,即是说,语法上看着像rvalue reference (“&&
”),但实际上却代表着一个lvalue reference (“&
”)。在这种情况下,此种引用比lvalue references 或者 rvalue references都要来的更灵活。
Rvalue references只能绑定到右值上,lvalue references除了可以绑定到左值上,在某些条件下还可以绑定到右值上。[1] 这里某些条件绑定右值为:常左值引用绑定到右值,非常左值引用不可绑定到右值!
例如:
string &s = "asd"; // error
const string &s = "asd"; // ok
规则简化如下:
左值引用 {左值}
右值引用 {右值}
常左值引用 {右值}
相比之下,声明中带 “&&
” 的,可能是lvalue references 或者 rvalue references 的引用可以绑定到任何东西上。这种引用灵活也忒灵活了,值得单独给它们起个名字。我称它们为 universal references(万能引用或转发引用、通用引用)。
拓展:在资料[6]中提到了const的重要性!
例如:
string f() { return "abc"; }
void g() {
const string &s = f(); // still legal?
cout << s << endl;
}
上面g函数中合法?
答案是合法的,原因是s是个左值,类型是常左值引用,而f()是个右值,前面提到常左值引用可以绑定到右值!所以合法,当然把const
去掉,便是不合法!
3.万能引用出现场合
到底 “&&
” 什么时候才意味着一个universal reference呢(即,代码当中的“&&
”实际上可能是 “&
”),具体细节还挺棘手的,所以这些细节我推迟到后面再讲。现在,我们还是先集中精力研究下下面的经验原则,因为你在日常的编程工作当中需要牢记它:
If a variable or parameter is declared to have type T&& for some deduced type
T
, that variable or parameter is a universal reference.
如果一个变量或者参数被声明为T&&,其中T是被推导的类型,那这个变量或者参数就是一个universal reference。
"T需要是一个被推导类型"这个要求限制了universal references的出现范围。
在实践当中,几乎所有的universal references都是函数模板的参数。因为auto
声明的变量的类型推导规则本质上和模板是一样的,所以使用auto的时候你也可能得到一个universal references。
这些在生产代码中并不常见,但我在本文里给出了一些例子,因为由auto声明的universal reference看着没有模板的那么啰嗦。在本文的Nitty Gritty Details p,
https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers#NittyGrittyDetails
我会讲解说明使用typedef和decltype的时候也可能会出现universal references,但在我们讲解这些繁琐的细节之前,我们可以暂时认为universal references只会出现在模板参数和由auto声明的变量当中。
一个universal reference必须具有形如T&&
,这个约束比它看起来要重要得多,但是我们稍后再对这一点进行详细的研究。现在,就先把这个约束记在脑子里吧。
和所有的引用一样,你必须对universal references进行初始化,而且正是universal reference的initializer决定了它到底代表的是lvalue reference 还是 rvalue reference:
如果用来初始化universal reference的表达式是一个左值,那么universal reference就变成lvalue reference。
如果用来初始化universal reference的表达式是一个右值,那么universal reference就变成rvalue reference。
上述可以根据下面代码例子理解:
template<typename T>
void f(T&& param);
假设你是initializer
:
int a;
f(a); // 传入左值,那么上述的T&& 就是lvalue reference,也就是左值引用绑定到了左值
f(1); // 传入右值,那么上述的T&& 就是rvalue reference,也就是右值引用绑定到了左值
4.理解左值与右值
4.1 精简版
只有在你能区分左值和右值的前提下,这个信息才有用。想要对这些术语进行精确定义是一件很难的事(c++11标准基本上是通过举例来说明一个表达式是否是一个lvalue还是rvalue的),但实践当中,下面的定义就足够了。
如果你可以对一个表达式取地址,那这个表达式就是个lvalue。
如果一个表达式的类型是一个lvalue reference (例如,
T&
或const T&
, 等.),那这个表达式就是一个lvalue。其它情况,这个表达式就是一个rvalue。从概念上来讲(通常实际上也是这样),rvalue对应于临时对象,例如函数返回值或者通过隐式类型转换得到的对象,大部分字面值(e.g.,
10
and5.3
)也是rvalues。
4.2 完整版
实际上,上述不太完整,标准里的定义实际更复杂,规定了下面这些值类别(value categories):
一个 lvalue 是通常可以放在等号左边的表达式,左值 一个 rvalue 是通常只能放在等号右边的表达式,右值 一个 glvalue 是 generalized lvalue,广义左值 一个 xvalue 是 expiring lvalue,将亡值 一个 prvalue 是 pure rvalue,纯右值
左值(lvalue)
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
变量、函数或数据成员
返回左值引用的表达式
如 ++x、x = 1、cout << ' '
int x = 0; cout << "(x).addr = " << &x << endl; cout << "(x = 1).addr = " << &(x = 1) << endl; cout << "(++x).addr = " << &++x << endl; //cout << "(x++).addr = " << &x++ << endl; // error cout << "(cout << ' ').addr=" << &(cout << ' ') << endl;
字符串字面量是左值,而且是不可被更改的左值。字符串字面量并不具名,但是可以用&取地址所以也是左值。
如 "hello",在c++中是 char const [6] 类型,而在c中是 char [6] 类型
cout << "(\"hello\").addr=" << &("hello") << endl;
如果一个表达式的类型是一个lvalue reference (例如,
T&
或const T&
, 等.),那这个表达式就是一个lvalue。
纯右值(prvalue)
反之,纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对 象”。最常见的情况有:
返回非引用类型的表达式
如 x++、x + 1
除字符串字面量之外的字面量如 42、true
将亡值(xvalue)
隐式或显式调用函数的结果,该函数的返回类型是对所返回对象类型的右值引用
int&& f(){
return 3;
}
int main()
{
f(); // The expression f() belongs to the xvalue category, because f() return type is