c++11新特性右值引用理解

  • 右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:
    1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
    2. ==能够更简洁明确地定义泛型函数。==

左值,右值的定义

  • C++( 包括 C) 中所有的表达式和变量==要么是左值,要么是右值==。
  • 通俗的左值的定义就是==非临时对象==,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。
  • 右值是指==临时的对象==,它们只在当前的语句中有效。
  • ==立即数==都是右值。
  • ==临时对象==是作为右值处理的
  • 在 C++11 之前,右值是不能被引用的,最大限度就是==用常量引用绑定一个右值==,如 :

const int &a = 1; 在这种情况下,右值不能被修改的。

右值引用能够方便地解决实际工程中的问题,实现非常有吸引力的解决方案。

  • 左值和右值的语法符号
  • 左值的声明符号为”&”, 为了和左值区分,==右值的声明符号为”&&”==
  • 如果临时对象通过一个接受右值的函数传递给另一个函数时,就会变成左值,因为这个临时对象在传递过程中,变成了命名对象。

右值引用的意义何在

  • 右值引用是用来支持==转移语义==的。
  • 转移语义可以将资源 ==(堆,系统对象等==)从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。
  • 临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
  • 转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝

你试试同一个盘符下面对一个大文件进行剪切/粘贴,复制/粘贴,可以明显看出速度的区别。在同一个文件系统下剪切/粘贴的过程只要对文件的记录进行修改就可以了,不用对数据块进行变动,自然会快很多。你可以想象剪切/粘贴和修改文件名是类似的。注意要在同一个文件系统(盘符)下试,不同的文件系统下这个操作就相当于复制/粘贴/删除的过程。自然会跟复制/粘贴差不多(删除也是个耗时很短的操作)

  • 通过转移语义,临时对象中的资源能够转移其它的对象里。

在现有的 C++ 机制中,我们可以定义==拷贝构造函数和赋值函数==。要实现转移语义,需==要定义转移构造函数,还可以定义转移赋值操作符==。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。

  • 普通的函数和操作符也==可以利用右值引用操作符实现转移语义==
  • 编译器区分了左值和右值,对右值调用了转移构造函数和转移赋值操作符。节省了资源,提高了程序运行的效率。
  • 有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。
    1. 参数(右值)的符号必须是右值引用符号,即“&&”。
    2. 参数(右值)不可以是常量,因为我们需要修改右值。\
    3. 参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。

既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,而==所有命名对象都只能是左值引用==,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 ==std::move==,这个函数以非常简单的方式==将左值引用转换为右值引用==。

  • 类的右值是一个临时对象,如果没有被绑定到引用,==在表达式结束时就会被废弃==。
  • 于是我们可以在右值被废弃之前,移走它的资源进行==废物利用==,从而避免无意义的复制。被移走资源的右值在废弃时==已经成为空壳==,析构的==开销也会降低==。

个人理解:采用移动构造可以直接取走临时对象的堆上内存,无需新申请,之后临时对象成为空壳不再拥有任何资源析构时也无需释放堆内存。


  • 右值引用意在解决==栈区对象==的所有权转移问题。
  • 对于==在堆区分配的对象==而言,转移对象所有权很简单,大致就是==指针赋值==。
  • 在C++11以前,==栈区的对象只能被deep copy==,而不能转移所有权,这就导致了一些确实要转移所有权而 deep copy 的不必要开销,比如函数返回临时对象给外界时会多次调用构造函数。

可以将栈上对象的堆资源直接转移,不需要对该对资源进行深拷贝,以及析构该堆资源。

  • 右值引用的神奇之处在于,在很多时候,使用函数库的程序员根本不用关心它的存在。
  • 原因即在于C++独有的值语义:程序员通过值语义可以方便直观地控制对象生命期,让RAII用起来更自然。

理解

在其他语言在面向堆空间、面向GC、面向引用编程的时候,C++在面向栈空间、面向生命周期管理、面向值类型的native编程路线上越走越远也越走越深,成为了一门时髦的编译语言。右值引用就是值类型编程中重要的一部分,没有它你就不能以很小的代价返回一个对象,以前我们都必须返回一个指针然后==要求调用方delete==,现在就不一样了,不管是直接返回对象还是返回智能指针都很安全。

C++ 的演化过程有点打补丁的感觉。先用一块补丁修补了C的不足,后来遇到问题,再打个补丁来修改。因为需要兼容以前的代码,不能直接拆掉旧补丁,而只能用新补丁直接覆盖在旧补丁上面。C++ 的功能特性,都有现实中的需求。当你没有遇到需求的时候,会觉得那个特性是多余的。但当遇到的时候,就会觉得有这种特性是多么美妙的事情。右值引用就是这样的特性。从前没有右值引用的时候,为避免临时对象的多余复制,和实现函数的完美转发,使了很多古怪手段,当有了右值引用,解决相同的问题,已经简单很多了。问题本身是复杂的,当不能用简单的手段来解决时,也只好用稍微复杂点的手段来解决了。这总比采用鸵鸟政策,当问题不存在要好。理解了右值引用需要解决什么问题,自然可以很好理解右值引用了。

右值引用,表面上看只是增加了一个引用符号,但它对 C++ 软件设计和类库的设计有非常大的影响。它既能简化代码,又能提高程序运行效率。每一个 C++ 软件设计师和程序员都应该理解并能够应用它。我们在设计类的时候如果有动态申请的资源,也应该设计转移构造函数和转移拷贝函数。在设计类库时,还应该考虑 std::move 的使用场景并积极使用它。

  1. 第一个问题就是临时对象非必要的昂贵的拷贝操作
  2. 第二个问题是在模板函数中如何按照参数的实际类型进行转发。

    • 非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值。
    • 而将亡值是C++11新增的、与右值引用相关的表达式,比如,将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值等。

T&& k = getVar();

  • 这里,getVar()产生的临时值不会像第一行代码那样,在表达式结束之后就销毁了,而是==会被“续命”==,他的生命周期将会通过右值引用得以延续,==和变量k的声明周期一样长==。
  • 通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。

右值引用独立于左值和右值。意思是右值引用类型的变量==可能是左值也可能是右值==

  • T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。
  • 需要注意的一个细节是,我们提供移动构造函数的同时==也会提供一个拷贝构造函数==,以防止移动不成功的时候还能拷贝构造(const 引用 才能接受右值),使我们的代码更安全。
  • 我们知道移动语义是通过右值引用来匹配临时值的,那么,==普通的左值是否也能借助移动语义来优化性能呢==,那该怎么做呢?事实上C++11为了解决这个问题,提供了std::move方法来将左值转换为右值,从而方便应用移动语义。move是将对象资源的所有权从一个对象转移到另一个对象,只是转移,没有内存的拷贝,==这就是所谓的move语义==。
{
    std::list< std::string> tokens;
    //省略初始化...
    std::list< std::string> t = tokens; //这里存在拷贝 
}
std::list< std::string> tokens;
std::list< std::string> t = std::move(tokens);  //这里没有拷贝
  • 如果不用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。他实际上将左值变成右值引用,然后应用移动语义,调用移动构造函数,就避免了拷贝,提高了程序性能。如果一个对象内部有较大的对内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。==事实上,C++11中所有的容器都实现了移动语义,方便我们做性能优化==
  • 支持移动构造和移动赋值
vector( vector&& other );
(C++11 起) 
(C++17 前)
vector( vector&& other ) noexcept;
(C++17 起)
vector( vector&& other, const Allocator& alloc );
(7) (C++11
  • ==move对于含资源(堆内存或句柄)的对象来说更有意义==
  • C++11之前调用模板函数时,存在一个比较头疼的问题,如何正确的传递参数
  • C++11引入了==完美转发==:在函数模板中,完全依照模板的参数的类型(==即保持参数的左值、右值特征==),将参数传递给函数模板中调用的另外一个函数。

右值引用T&&是一个universal references,==可以接受左值或者右值,正是这个特性让他适合作为一个参数的路由==,然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发


  • 我们可以结合完美转发和移动语义来实现==一个泛型的工厂函数==,这个工厂函数可以创建所有类型的对象。
template<typename…  Args>
T* Instance(Args&&… args)
{
    return new T(std::forward<Args >(args)…);
}
  • 这个工厂函数的参数是右值引用类型,内部使用std::forward按照参数的实际类型进行转发,如果参数的实际类型是右值,那么创建的时候会自动匹配移动构造,如果是左值则会匹配拷贝构造。
  • 参考;https://www.cnblogs.com/qicosmos/p/4283455.html

猜你喜欢

转载自blog.csdn.net/zhc_24/article/details/81183082
今日推荐