C++11右值引用和转发型引用教程详解

右值引用

为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念。右值引用采用T&&这一语法形式,比传统的引用T&(如今被称作左值引用 lvalue reference)多一个&。

如果把经由T&&这一语法形式所产生的引用类型都叫做右值引用,那么这种广义的右值引用又可分为以下三种类型:

  • 无名右值引用
  • 具名右值引用
  • 转发型引用

无名右值引用和具名右值引用的引入主要是为了解决移动语义问题。

转发型引用的引入主要是为了解决完美转发问题。

无名右值引用

无名右值引用(unnamed rvalue reference)是指由右值引用相关操作所产生的引用类型。

无名右值引用主要通过返回右值引用的类型转换操作产生, 其语法形式如下:

static_cast<T&&>(t)

标准规定该语法形式将把表达式 t 转换为T类型的无名右值引用。

无名右值引用是右值,标准规定无名右值引用和传统的右值一样具有潜在的可移动性,即它所占有的资源可以被移动(窃取)。

std::move()

由于无名右值引用是右值,借助于类型转换操作产生无名右值引用这一手段,左值表达式就可以被转换成右值表达式。为了便于利用这一重要的转换操作,标准库为我们提供了封装这一操作的函数,这就是std::move()。

假设左值表达式 t 的类型为T&,利用以下函数调用就可以把左值表达式 t 转换为T类型的无名右值引用(右值,类型为T&&)。
std::move(t)

具名右值引用

如果某个变量或参数被声明为T&&类型,并且T无需推导即可确定,那么这个变量或参数就是一个具名右值引用(named rvalue reference)。

具名右值引用是左值,因为具名右值引用有名字,和传统的左值引用一样可以用操作符&取地址。

与广义的右值引用相对应,狭义的右值引用仅限指具名右值引用。

传统的左值引用可以绑定左值,在某些情况下也可绑定右值。与此不同的是,右值引用只能绑定右值。

右值引用和左值引用统称为引用(reference),它们具有引用的共性,比如都必须在初始化时绑定值,都是左值等等。

struct X {};
X a;
X&& b = static_cast<X&&>(a);
X&& c = std::move(a);
//static_cast<X&&>(a) 和 std::move(a) 是无名右值引用,是右值
//b 和 c 是具名右值引用,是左值
X& d = a;
X& e = b;
const X& f = c;
const X& g = X();
X&& h = X();
//左值引用d和e只能绑定左值(包括传统左值:变量a以及新型左值:右值引用b)
//const左值引用f和g可以绑定左值(右值引用c),也可以绑定右值(临时对象X())
//右值引用b,c和h只能绑定右值(包括新型右值:无名右值引用std::move(a)以及传统右值:临时对象X()) 

左右值重载策略

有时我们需要在函数中区分参数的左右值属性,根据参数左右值属性的不同做出不同的处理。适当地采用左右值重载策略,借助于左右值引用参数不同的绑定特性,我们可以利用函数重载来做到这一点。常见的左右值重载策略如下:

struct X {};
//左值版本
void f(const X& param1){/*处理左值参数param1*/}
//右值版本
void f(X&& param2){/*处理右值参数param2*/}
X a;
f(a);      //调用左值版本
f(X());     //调用右值版本
f(std::move(a)); //调用右值版本 

即在函数重载中分别重载const左值引用和右值引用。

重载const左值引用的为左值版本,这是因为const左值引用参数能绑定左值,而右值引用参数不能绑定左值。

重载右值引用的为右值版本,这是因为虽然const左值引用参数和右值引用参数都能绑定右值,但标准规定右值引用参数的绑定优先度要高于const左值引用参数。

移动构造器和移动赋值运算符

在类的构造器和赋值运算符中运用上述左右值重载策略,就会产生两个新的特殊成员函数:移动构造器(move constructor)和移动赋值运算符(move assignment operator)。

struct X
{
  X();             //缺省构造器
  X(const X& that);      //拷贝构造器
  X(X&& that);         //移动构造器
  X& operator=(const X& that); //拷贝赋值运算符
  X& operator=(X&& that);   //移动赋值运算符
};
X a;               //调用缺省构造器
X b = a;             //调用拷贝构造器
X c = std::move(b);       //调用移动构造器
b = a;              //调用拷贝赋值运算符
c = std::move(b);        //调用移动赋值运算符 

移动语义

无名右值引用和具名右值引用的引入主要是为了解决移动语义问题。

移动语义问题是指在某些特定情况下(比如用右值来赋值或构造对象时)如何采用廉价的移动语义替换昂贵的拷贝语义的问题。

移动语义(move semantics)是指某个对象接管另一个对象所拥有的外部资源的所有权。移动语义需要通过移动(窃取)其他

对象所拥有的资源来完成。移动语义的具体实现(即一次that对象到this对象的移动(move))通常包含以下若干步骤:

  • 如果this对象自身也拥有资源,释放该资源
  • 将this对象的指针或句柄指向that对象所拥有的资源
  • 将that对象原本指向该资源的指针或句柄设为空值

上述步骤可简单概括为①释放this(this非空时)②移动that

移动语义通常在移动构造器和移动赋值运算符中得以具体实现。两者的区别在于移动构造对象时this对象为空因而①释放this无须进行。

与移动语义相对,传统的拷贝语义(copy semantics)是指某个对象拷贝(复制)另一个对象所拥有的外部资源并获得新生资源的所有权。拷贝语义的具体实现(即一次that对象到this对象的拷贝(copy))通常包含以下若干步骤:

  • 如果this对象自身也拥有资源,释放该资源
  • 拷贝(复制)that对象所拥有的资源
  • 将this对象的指针或句柄指向新生的资源
  • 如果that对象为临时对象(右值),那么拷贝完成之后that对象所拥有的资源将会因that对象被销毁而即刻得以释放

上述步骤可简单概括为①释放this(this非空时)②拷贝that③释放that(that为右值时)
拷贝语义通常在拷贝构造器和拷贝赋值运算符中得以具体实现。两者的区别在于拷贝构造对象时this对象为空因而①释放this无须进行。

比较移动语义与拷贝语义的具体步骤可知,在赋值或构造对象时,

如果源对象that为左值,由于两者效果不同(移动that ≠ 拷贝that),此时移动语义不能用来替换拷贝语义。

如果源对象that为右值,由于两者效果相同(移动that = 拷贝that + 释放that),此时廉价的移动语义(通过指针操作来移动资源)便可以用来替换昂贵的拷贝语义(生成,拷贝然后释放资源)。

由此可知,只要在进行相关操作(比如赋值或构造)时,采取适当的左右值重载策略区分源对象的左右值属性,根据其左右值属性分别采用拷贝语义和移动语义,移动语义问题便可以得到解决。

下面用MemoryBlock这个自我管理内存块的类来具体说明移动语义问题。

#include <iostream>
class MemoryBlock
{
public:
  // 构造器(初始化资源)
  explicit MemoryBlock(size_t length)
    : _length(length)
    , _data(new int[length])
  {
  }
  // 析构器(释放资源)
  ~MemoryBlock()
  {
    if (_data != nullptr)
    {
      delete[] _data;
    }
  }
  // 拷贝构造器(实现拷贝语义:拷贝that)
  MemoryBlock(const MemoryBlock& that)
    // 拷贝that对象所拥有的资源
    : _length(that._length)
    , _data(new int[that._length])
  {
    std::copy(that._data, that._data + _length, _data);
  }
  // 拷贝赋值运算符(实现拷贝语义:释放this + 拷贝that)
  MemoryBlock& operator=(const MemoryBlock& that)
  {
    if (this != &that)
    {
      // 释放自身的资源
      delete[] _data;
      // 拷贝that对象所拥有的资源
      _length = that._length;
      _data = new int[_length];
      std::copy(that._data, that._data + _length, _data);
    }
    return *this;
  }
  // 移动构造器(实现移动语义:移动that)
  MemoryBlock(MemoryBlock&& that)
    // 将自身的资源指针指向that对象所拥有的资源
    : _length(that._length)
    , _data(that._data)
  {
    // 将that对象原本指向该资源的指针设为空值
    that._data = nullptr;
    that._length = 0;
  }
  // 移动赋值运算符(实现移动语义:释放this + 移动that)
  MemoryBlock& operator=(MemoryBlock&& that)
  {
    if (this != &that)
    {
      // 释放自身的资源
      delete[] _data;
      // 将自身的资源指针指向that对象所拥有的资源
      _data = that._data;
      _length = that._length;
      // 将that对象原本指向该资源的指针设为空值
      that._data = nullptr;
      that._length = 0;
    }
    return *this;
  }
private:
  size_t _length; // 资源的长度
  int* _data; // 指向资源的指针,代表资源本身
};
MemoryBlock f() { return MemoryBlock(50); }
int main()
{
  MemoryBlock a = f();      // 调用移动构造器,移动语义
  MemoryBlock b = a;       // 调用拷贝构造器,拷贝语义
  MemoryBlock c = std::move(a);  // 调用移动构造器,移动语义
  a = f();            // 调用移动赋值运算符,移动语义
  b = a;             // 调用拷贝赋值运算符,拷贝语义
  c = std::move(a);        // 调用移动赋值运算符,移动语义
} 

转发型引用

如果某个变量或参数被声明为T&&类型,并且T需要经过推导才可确定,那么这个变量或参数就是一个转发型引用(forwarding reference)。

转发型引用由以下两种语法形式产生

  • 如果某个变量被声明为auto&&类型,那么这个变量就是一个转发型引用
  • 在函数模板中,如果某个参数被声明为T&&类型,并且T是一个需要经过推导才可确定的模板参数类型,那么这个参数就是一个转发型引用

转发型引用是不稳定的,它的实际类型由它所绑定的值来确定。转发型引用既可以绑定左值,也可以绑定右值。如果绑定左值,转发型引用就成了左值引用。如果绑定右值,转发型引用就成了右值引用。
转发型引用在被C++标准所承认之前曾经被称作万能引用(universal reference)。万能引用这一术语的发明者,Effective C++系列的作者Scott Meyers认为,如此异常灵活的引用类型不属于右值引用,它应该拥有自己的名字。

对于某个转发型引用类型的变量(auto&&类型)来说

  • 如果初始化表达式为左值(类型为U&),该变量将成为左值引用(类型为U&)。
  • 如果初始化表达式为右值(类型为U&&),该变量将成为右值引用(类型为U&&)。

对于函数模板中的某个转发型引用类型的形参(T&&类型)来说

  • 如果对应的实参为左值(类型为U&),模板参数T将被推导为引用类型U&,该形参将成为左值引用(类型为U&)。
  • 如果对应的实参为右值(类型为U&&),模板参数T将被推导为非引用类型U,该形参将成为右值引用(类型为U&&)。
struct X {};
X&& var1 = X();              // var1是右值引用,只能绑定右值X()
auto&& var2 = var1;            // var2是转发型引用,可以绑定左值var1
                      // var2的实际类型等同于左值var1,即X&
auto&& var3 = X();             // var3是转发型引用,可以绑定右值X()
                      // var3的实际类型等同于右值X(),即X&&
template<typename T>
void g(std::vector<typename T>&& param1); // param1是右值引用
template<typename T>
void f(T&& param2);            // param2是转发型引用
X a;
f(a);        // 模板函数f()的形参param2是转发型引用,可以绑定左值a
           // 在此次调用中模板参数T将被推导为引用类型X&
           // 而形参param2的实际类型将等同于左值a,即X&
f(X());       // 模板函数f()的形参param2是转发型引用,可以绑定右值X()
           // 在此次调用中模板参数T将被推导为非引用类型X
           // 而形参param2的实际类型将等同于右值X(),即X&&
// 更多右值引用和转发型引用
const auto&& var4 = 10;              // 右值引用
template<typename T>
void h(const T&& param1);             // 右值引用
template <typename T/*, class Allocator = allocator*/>
class vector
{
public:
  void push_back( T&& t );           // 右值引用
  template <typename Args...>
  void emplace_back( Args&&... args );     // 转发型引用
}; 

完美转发

完美转发(perfect forwarding)问题是指函数模板在向其他函数转发(传递)自身参数(形参)时该如何保留该参数(实参)的左右值属性的问题。也就是说函数模板在向其他函数转发(传递)自身形参时,如果相应实参是左值,它就应该被转发为左值;同样如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)的可能性。如果将自身参数不分左右值一律转发为左值,其他函数就只能将转发而来的参数视为左值,从而失去针对该参数的左右值属性进行不同处理的可能性。

转发型引用的引入主要是为了解决完美转发问题。在函数模板中需要保留左右值属性的参数,也就是要被完美转发的参数须被声明为转发型引用类型,即参数必须被声明为T&&类型,而T必须被包含在函数模板的模板参数列表之中。按照转发型引用类型形参的特点,该形参将根据所对应的实参的左右值属性而分别蜕变成左右值引用。但无论该形参成为左值引用还是右值引用,该形参在函数模板内都将成为左值。这是因为该形参有名字,左值引用是左值,具名右值引用也同样是左值。如果在函数模板内照原样转发该形参,其他函数就只能将转发而来的参数视为左值,完美转发任务将会失败。

#include<iostream>
using namespace std;
struct X {};
void inner(const X&) {cout << "inner(const X&)" << endl;}
void inner(X&&) {cout << "inner(X&&)" << endl;}
template<typename T>
void outer(T&& t) {inner(t);}
int main()
{
  X a;
  outer(a);
  outer(X());
}
//inner(const X&)
//inner(const X&)
std::forward()

要在函数模板中完成完美转发转发型引用类型形参的任务,我们必须在相应实参为左值,该形参成为左值引用时把它转发成左值,在相应实参为右值,该形参成为右值引用时把它转发成右值。此时我们需要标准库函数std::forward()。

标准库函数 std::forward<T>(t) 有两个参数:模板参数 T 与 函数参数 t。函数功能如下:

  • 当T为左值引用类型U&时,t 将被转换为无名左值引用(左值,类型为U&)。
  • 当T为非引用类型U或右值引用类型U&&时,t 将被转换为无名右值引用(右值,类型为U&&)。

使用此函数,我们在函数模板中转发类型为T&&的转发型引用参数 t 时,只需将参数 t 替换为std::forward<T>(t)即可完成完美转发任务。这是因为

  • 如果 t 对应的实参为左值(类型为U&),模板参数T将被推导为引用类型U&,t 成为具名左值引用(类型为U&),std::forward<T>(t)就会把 t 转换成无名左值引用(左值,类型为U&)。
  • 如果 t 对应的实参为右值(类型为U&&),模板参数T将被推导为非引用类型U,t 成为具名右值引用(类型为U&&),std::forward<T>(t)就会把 t 转换成无名右值引用(右值,类型为U&&)。
#include<iostream>
using namespace std;
struct X {};
void inner(const X&) {cout << "inner(const X&)" << endl;}
void inner(X&&) {cout << "inner(X&&)" << endl;}
template<typename T>
void outer(T&& t) {inner(forward<T>(t));}
int main()
{
  X a;
  outer(a);
  outer(X());
}
//inner(const X&)
//inner(X&&) 

总结

以上所述是小编给大家介绍的C++11右值引用和转发型引用教程详解,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • 深入解读C++中的右值引用

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来. 从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 在标准C++语言中,临时量(术语为右值,因其出

  • 详解C++11中的右值引用与移动语义

    C++11的一个最主要的特性就是可以移动而非拷贝对象的能力.很多情况都会发生对象的拷贝,有时对象拷贝后就立即销毁,在这些情况下,移动而非拷贝对象会大幅度提升性能. 右值与右值引用 为了支持移动操作,新标准引入了一种新的引用类型--右值引用,就是必须绑定到右值的引用.我们通过&&而不是&来获得右值引用.右值引用一个重要的特性就是只能绑定到将要销毁的对象. 左值和右值是表达式的属性,一些表达式生成或要求左值,而另一些则生成或要求右值.一般而言,一个左值表达式表示的是一个对象的身份,而右

  • C++标准之(ravalue reference) 右值引用介绍

    1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了CopyElision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下CopyElision.RVO,对此不感兴趣的可以直接跳过: (1)CopyElision CopyElision技术是为了防止某些不必要的临时对象产生和拷贝,例如: 复制代码 代码如下: structA{ A(

  • 浅析C++11中的右值引用、转移语义和完美转发

    1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值. 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如: int& fo

  • 浅谈C++左值引用和右值引用

    实例如下: #include<iostream> #include<utility> #include<vector> using namespace std; int f(); int main() { vector<int>vi(100); int i=42; int &&r1=i;//error不能把右值引用绑到左值上 int &&r2=10; int &r3=i; int &r4=10;//error非

  • C++11右值引用和std::move语句实例解析(推荐)

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一.从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 下面用实例来深入探讨右值引用. 1.什么是左值,什么是右值,简单说左值可以赋值,右值不可以赋值.以下面代码为例,"A a = getA();&q

  • C++11右值引用和转发型引用教程详解

    右值引用 为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念.右值引用采用T&&这一语法形式,比传统的引用T&(如今被称作左值引用 lvalue reference)多一个&. 如果把经由T&&这一语法形式所产生的引用类型都叫做右值引用,那么这种广义的右值引用又可分为以下三种类型: 无名右值引用 具名右值引用 转发型引用 无名右值引用和具名右值引用的引入主要是为了解决移动语义问题. 转发型引用的引

  • C++11右值引用和移动语义的实例解析

    目录 基本概念 左值 vs 右值 左值引用 vs 右值引用 右值引用使用场景和意义 左值引用的使用场景 左值引用的短板 右值引用和移动语义 右值引用引用左值 右值引用的其他使用场景 完美转发 万能引用 完美转发保持值的属性 完美转发的使用场景 总结 基本概念 左值 vs 右值 什么是左值? 左值是一个表示数据的表达式,如变量名或解引用的指针. 左值可以被取地址,也可以被修改(const修饰的左值除外). 左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边. int main() { //以

  • Java中的引用和动态代理的实现详解

    我们知道,动态代理(这里指JDK的动态代理)与静态代理的区别在于,其真实的代理类是动态生成的.但具体是怎么生成,生成的代理类包含了哪些内容,以什么形式存在,它为什么一定要以接口为基础? 如果去看动态代理的源代码(java.lang.reflect.Proxy),会发现其原理很简单(真正二进制类文件的生成是在本地方法中完成,源代码中没有),但其中用到了一个缓冲类java.lang.reflect.WeakCache<ClassLoader,Class<?>[],Class<?>

  • C++ 中引用与指针的区别实例详解

    C++ 中引用与指针的区别实例详解 引用是从C++才引入的,在C中不存在.为了搞清楚引用的概念,得先搞明白变量的定义及引用与变量的区别,变量的要素一共有两个:名称与空间. 引用不是变量,它仅仅是变量的别名,没有自己独立的空间,它只符合变量的"名称"这个要素,而"空间"这个要素并不满足.换句话说,引用需要与它所引用的变量共享同一个内存空间,对引用所做的改变实际上是对所引用的变量做出修改.并且引用在定义的时候就必须被初始化.     参数传递的类型及相关要点: 1 按值

  • C语言指针与引用的区别以及引用的三种用法案例详解

    1.指针与引用的区别: 指针是一块内存的地址值,而引用是一块内存的别名. 下面引自://www.jb51.net/article/221791.htm 从概念上讲.指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变. 而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量). 在C++中,指针和引用经

  • C++ 中引用和指针的关系实例详解

    C++ 中引用和指针的关系实例详解 1.引用在定义时必须初始化,指针没有要求 int &rNum; //未初始化不能通过编译 int *pNum; //可以 2. 一旦一个引用被初始化为指向一个对象,就不能再指向 其他对象,而指针可以在任何时候指向任何一个同类型对象 int iNum = 10; int iNum2 = 20; int &rNum = iNum; &rNum = iNum2; //不能通过 3. 没有NULL引用,但有NULL指针. int *pNum = NULL

  • Win7系统下mysql 5.7.11安装教程详解

    操作系统:win7 64位旗舰版 mysql压缩包:mysql-5.7.11-winx64.zip 1. 解压MySQL压缩包 将下载的MySQL压缩包解压到自定义目录下,我的解压目录是: "D:\Program Files\mysql-5.7.11-winx64" 将解压目录下默认文件 my-default.ini 拷贝一份,改名 my.ini 复制下面的配置信息到 my.ini 保存 #如果没有my-default.ini,可自己新建my.ini或者从其他地方中获取 #######

  • vue中引用swiper轮播插件的教程详解

    有时候我们需要在vue中使用轮播组件,如果是在vue组件中引入第三方组件的话,最好通过npm安装,从而进行统一安装包管理. 申明:本文所使用的是vue.2x版本. 通过npm安装插件:  npm install swiper --save-dev 在需要使用swiper的组件里引入swiper,swiper的初始化放在mounted里 Slider.vue源码: <template> <div class="swiper-container"> <div

  • vue2中引用及使用 better-scroll的方法详解

    使用时有三个要点: 一:html部分 <div class="example" ref="divScroll"> <div> <p>内容1</p> <p>内容2</p> <ul> <li>list1</li> <li>list2</li> <ul> </div> </div> [注] 1.最外层加re

随机推荐