右值引用、移动语义、万能引用与完美转发

一、右值引用

1.右值与右值引用

在C++11中,右值分为两个概念:将亡值(xvalue, eXpiring Value)和纯右值(prvalue, Pure Rvalue)。

纯右值是C++98中右值的概念,表示用于辨识临时变量和一些不跟对象关联的值,临时变量如非引用返回的函数返回的临时变量值、一些运算表达式产生的临时变量的值,不跟对量关联的字面常量如:2‘c’true

将亡值是C++11新增的跟右值引用相关的表达式,将亡值可以理解为通过移动构造其他变量内存空间的方式获取到的值,在确保其他变量不再被使用、或即将被销毁时,来延长变量值的生命期。而实际上该右值会马上被销毁,所以称之为将亡值。

所谓右值引用就是必须绑定到优质的引用,右值引用的一个重要特性就是只能绑定到一个将要销毁的对象,因此使用右值引用的代码可以自由地接管所引用的对象的资源。

2.为什么有右值引用

假设定义了函数void foo(Test &t){...};,通过传递引用来提高效率,若以foo(Test())的形式调用函数,会编译不过,因为这是将引用绑定到一个匿名对象,它可能很快就不在了,所以没有意义。为了解决这一问题,定义重载函数void foo(Test t),仍会编译不过,因为这样当以foo(t)形式调用函数时,会有二义性。

引入右值引用可以解决上述问题,用void foo(Test&& t)void foo(Test& t)两个函数来区分传入值是否是将亡值,并且可以重载,无二义性。

3.左值引用与右值引用区别

左值引用是起别名,如果这个对象已经析构,那么这个别名也应该一起失效,也就是说左值引用一定要保证它的生命周期小于等于它被引用的对象。

当将亡值出现的时候,左值引用表示无能为力,所以右值引用出现了。

右值引用也可以看作起名,只是它起名的对象是一个将亡值,然后延续这个将亡值的生命,直到这个的右值的生命也结束了。

除了入参时可以用到右值引用外,其他右值引用都显得多余。

比如Test&& t{Test()};它和Test t{Test()}是一样的,甚至可以这样Test&& t{}它们都只一个普通对象,只调用一次构造函数。

二、移动语义

1.移动构造函数和移动赋值运算符

拷贝对象时在某些情况下,对象拷贝后就立即被销毁了,此时从旧内存将元素拷贝到新内存是不必要的,更好的方式是移动元素。另一方面,对于IO类、std::unique_ptr这样的类,他们都包含不能被共享的资源,因此,这些类型的对象不能拷贝但可以移动。

为了让我们自己的类型支持移动操作,需要为其定义移动构造函数和移动赋值运算符。

移动构造函数的第一个参数是该类类型的一个引用,且是一个右值引用。在移动构造函数中,获取了被移动对象的资源(这里是内存)的所有权;在接管内存之后,将给定对象中的指针都置为nullptr(使其成为析构安全地状态),这样就完成了从给定对象的移动操作。最终,移后源对象会被销毁,意味着在其上运行析构函数。

移动赋值运算符执行与析构函数和移动构造函数相同的工作。

2.std::move()

std::move(lvalue)的作用是把一个左值转换为右值。当局部变量的左值想移动而不是拷贝时,出现std::move将左值转为右值

// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
    return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

std::move是一个模板函数,通过remove_\reference_t获得模板参数的原本类型,然后把值转换为该类型的右值。使用std::move意味着,把一个左值转换为右值,原先的值不应该继续再使用。

三、万能引用

1.万能引用

C++ 11中有万能引用(Universal Reference)的概念:使用T&&类型的形参既能绑定右值,又能绑定左值。但是注意了:只有发生类型推导的时候,T&&才表示万能引用;否则,表示右值引用。

template<typename T>
void func(T&& param) {
    cout << param << endl;
}

int main() {
    int num = 2019;
    func(num);
    func(2019);
    return 0;
}

2.引用折叠

一个模板函数,根据定义的形参和传入的实参的类型,我们可以有下面四中组合:

  • 左值-左值 T& &     -- T& # 函数定义的形参类型是左值引用,传入的实参是左值引用
  • 左值-右值 T& &&   -- T&  # 函数定义的形参类型是左值引用,传入的实参是右值引用
  • 右值-左值 T&& &   -- T&  # 函数定义的形参类型是右值引用,传入的实参是左值引用
  • 右值-右值 T&& &&  -- T&& # 函数定义的形参类型是右值引用,传入的实参是右值引用

但是C++中不允许对引用再进行引用,对于上述情况的处理有如下的规则:

所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。规则是:如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。

四、完美转发

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。
一个例子

template<typename T>
void func(T& param) {
    cout << "传入的是左值" << endl;
}
template<typename T>
void func(T&& param) {
    cout << "传入的是右值" << endl;
}

template<typename T>
void warp(T&& param) {
    func(param);
}

int main() {
    int num = 2019;
    warp(num);
    warp(2019);
    return 0;
}

输出结果:

传入的是左值
传入的是左值

warp()函数本身的形参是一个万能引用,即可以接受左值又可以接受右值;第一个warp()函数调用实参是左值,所以,warp()函数中调用func()中传入的参数也应该是左值;第二个warp()函数调用实参是右值,根据上面所说的引用折叠规则,warp()函数接收的参数类型是右值引用,但在warp()函数内部,因为参数有了名称,左值引用类型变为了右值,我们也通过变量名取得变量地址。

这就是我们所谓的“完美转发”技术,可以保持函数调用过程中,变量类型的不变,在C++11中通过std::forward()函数来实现。修改warp()函数如下:

template<typename T>
void warp(T&& param) {
    func(std::forward<T>(param));
}

五、总结

  • 右值引用可以解决传入的函数参数是将亡值的问题
  • 右值引用可能是左值也可能是右值,若这个右值引用被命名了,它就是左值
  • 移动语义可以减少无谓的内存拷贝,要想实现移动语义,需要实现移动构造函数和移动赋值运算符
  • 若想对一个左值进行移动构造,可用std::move将一个左值转换成一个右值
  • 使用T&&类型的形参既能绑定右值,又能绑定左值。只有发生类型推导的时候,才表示万能引用;
  • 引用折叠规则:所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它将变成左值引用,输入右值则变成具名的右值应用。
  • 通过一个函数将参数继续转交给另一个函数进行处理,如果继续保持参数的原有特征,就是完美转发。std::forward()和万能引用共同实现完美转发。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,565评论 6 525
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,696评论 3 406
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,935评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,327评论 1 303
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,338评论 6 401
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,760评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,085评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,091评论 0 280
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,656评论 1 327
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,657评论 3 348
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,767评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,360评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,088评论 3 341
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,493评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,654评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,374评论 3 383
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,841评论 2 367

推荐阅读更多精彩内容