现代C++之万能引用、完美转发、引用折叠(万字长文)

现代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 and 5.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
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值