深入了解c++11 移动语义与右值引用

1.移动语义

C++11新标准中一个最主要的特性就是提供了移动而非拷贝对象的能力。如此做的好处就是,在某些情况下,对象拷贝后就立即被销毁了,此时如果移动而非拷贝对象会大幅提升性能。参考如下程序:

//moveobj.cpp

#include <iostream>
#include <vector>
using namespace std;

class Obj
{
public:
 Obj(){cout <<"create obj" << endl;}
 Obj(const Obj& other){cout<<"copy create obj"<<endl;}
};

vector<Obj> foo()
{
 vector<Obj> c;
   c.push_back(Obj());
 cout<<"---- exit foo ----"<<endl;
   return c;
}

int main()
{
 vector<Obj> v;
 v=foo();
}

编译并运行:

[b3335@localhost test]$ g++ moveobj.cpp
[b3335@localhost test]$ ./a.out
create obj
copy create obj
---- exit foo ----
copy create obj

可见,对obj对象执行了两次拷贝构造。vector是一个常用的容器了,我们可以很容易的分析这这两次拷贝构造的时机:
 (1)第一次是在函数foo中通过临时Obj的对象Obj()构造一个Obj对象并入vector中;
 (2)第二次是通过从函数foo中返回的临时的vector对象来给v赋值时发生了元素的拷贝。

由于对象的拷贝构造的开销是非常大的,因此我们想就可能避免他们。其中,第一次拷贝构造是vector的特性所决定的,不可避免。但第二次拷贝构造,在C++ 11中就是可以避免的了。

[b3335@localhost test]$ g++ -std=c++11 moveobj.cpp
[b3335@localhost test]$ ./a.out
create obj
copy create obj
---- exit foo ----

可以看到,我们除了加上了一个-std=c++11选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。它是如何实现这一过程的呢?

在老版本中,当我们执行第二行的赋值操作的时候,执行过程如下:

(1)foo()函数返回一个临时对象(这里用tmp来标识它);
 (2)执行vector的 ‘=' 函数,将对象v中的现有成员删除,将tmp的成员复制到v中来;
 (3)删除临时对象tmp。

在C++11的版本中,执行过程如下:

(1)foo()函数返回一个临时对象(这里用tmp来标识它);
 (2)执行vector的 ‘=' 函数,释放对象v中的成员,并将tmp的成员移动到v中,此时v中的成员就被替换成了tmp中的成员;
 (3)删除临时对象tmp。

关键的过程就是第2步,它是移动而不是复制,从而避免了成员的拷贝,但效果却是一样的。不用修改代码,性能却得到了提升,对于程序员来说就是一份免费的午餐。但是,这份免费的午餐也不是无条件就可以获取的,需要带上-std=c++11来编译。

2.右值引用

2.1右值引用简介

为了支持移动操作,C++11引入了一种新的引用类型——右值引用(rvalue reference)。所谓的右值引用指的是必须绑定到右值的引用。使用&&来获取右值引用。这里给右值下个定义:只能出现在赋值运算符右边的表达式才是右值。相应的,能够出现在赋值运算符左边的表达式就是左值,注意,左值也可以出现在赋值运算符的右边。对于常规引用,为了与右值引用区别开来,我们可以称之为左值引用(lvalue reference)。下面是左值引用与右值引用示例:

int i=42;
int& r=i;  //正确,左值引用
int&& rr=i;  //错误,不能将右值引用绑定到一个左值上
int& r2=i*42; //错误,i*42是一个右值
const int& r3=i*42; //正确:可以将一个const的引用绑定到一个右值上
int&& rr2=i*42; //正确:将rr2绑定到乘法结果上

从上面可以看到左值与右值的区别有:
 (1)左值一般是可寻址的变量,右值一般是不可寻址的字面常量或者是在表达式求值过程中创建的可寻址的无名临时对象;
 (2)左值具有持久性,右值具有短暂性。

不可寻址的字面常量一般会事先生成一个无名临时对象,再对其建立右值引用。所以右值引用一般绑定到无名临时对象,无名临时对象具有如下两个特性:

(1)临时对象将要被销毁;
 (2)临时对象无其他用户。

这两个特性意味着,使用右值引用的代码可以自由地接管所引用的对象的资源。

2.2 std::move 强制转化为右值引用

虽然不能直接对左值建立右值引用,但是我们可以显示地将一个左值转换为对应的右值引用类型。我们可以通过调用C++11在标准库中<utility>中提供的模板函数std::move来获得绑定到左值的右值引用。示例如下:

int&& rr1=42;
int&& rr2=rr1;				//error,表达式rr1是左值
int&& rr2=std::move(rr1);	//ok

上面的代码说明了右值引用也是左值,不能对右值引用建立右值引用。move告诉编译器,在对一个左值建立右值引用后,除了对左值进行销毁和重新赋值,不能够再访问它。std::move在VC10.0版本的STL库中定义如下:

/*
 * @brief Convert a value to an rvalue.
 * @param __t A thing of arbitrary type.
 * @return The parameter cast to an rvalue-reference to allow moving it.
*/
template<typename _Tp> constexpr typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) noexcept{
	return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}

template<class _Ty> struct remove_reference{
  // remove reference
  typedef _Ty type;
};

template<class _Ty> struct remove_reference<_Ty&>{
  // remove reference
  typedef _Ty type;
};

template<class _Ty> struct remove_reference<_Ty&&>{
  // remove rvalue reference
  typedef _Ty type;
};

move的参数是接收一个任意类型的右值引用,通过引用折叠,此参数可以与任意类型实参匹配。特别的,我们既可以传递左值,也可以传递右值给std::move:

string s1("hi");
string&& s2=std::move(string("bye")); //正确:从一个右值移动数据
string&& s3=std::move(s1);  //正确:在赋值之后,s1的值是不确定的

注意:
 (1)std::move函数名称具有一定迷惑性,实际上std::move并没有移动任何东西,本质上就是一个static_cast<T&&>,它唯一的功能是将一个左值强制转化为右值引用,进而可以使用右值引用使用该值,以用于移动语义。

(2)typename为什么会出现在std::move返回值前面?这里需要明白typename的两个作用,一个是申明模板中的类型参数,二是在模板中标明“内嵌依赖类型名”(nested dependent type name)[3]^{[3]}[3]。“内嵌依赖类型名”中“内嵌”是指类型定义在类中。以上type是定义在struct remove_reference;“依赖”是指依赖于一个模板参数,上面的std::remove_reference<_Tp>::type&&依赖模板参数_Tp。“类型名”是指这里最终要使用的是个类型名,而不是变量。

2.3 std::forward实现完美转发

完美转发(perfect forwarding)指在函数模板中,完全依照模板参数的类型,将参数传递给函数模板中调用的另外一个函数,如:

template<typename T> void IamForwording(T t)
{
 IrunCodeActually(t);
}

其中,IamForwording是一个转发函数模板,函数IrunCodeActually则是真正执行代码的目标函数。对于目标函数IrunCodeActually而言,它总是希望获取的参数类型是传入IamForwording时的参数类型。这似乎是一件简单的事情,实际并非如此。为何还要进行完美转发呢?因为右值引用本身是个左值,当一个右值引用类型作为函数的形参,在函数内部再转发该参数的时候它实际上是一个左值,并不是它原来的右值引用类型了。考察如下程序:

template<typename T>
void PrintT(T& t)
{
 cout << "lvalue" << endl;
}

template<typename T>
void PrintT(T && t)
{
 cout << "rvalue" << endl;
}

template<typename T>
void TestForward(T&& v)
{
 PrintT(v);
}

int main()
{
 TestForward(1); //输出lvaue,理应输出rvalue
}

实际上,我们只需要使用函数模板std::forward即可完成完美转发,按照参数本来的类型转发出去,考察如下程序:

template<typename T>
void TestForward(T&& v)
{
 PrintT(std::forward<T>(v));
}

int main()
{
 TestForward(1); //输出rvalue
 int x=1;
 TestForward(x); //输出lvalue
}

下面给出std::forward的简单实现:

template<typename T>
struct RemoveReference
{
 typedef T Type;
};

template<typename T>
struct RemoveReference<T&>
{
 typedef T Type;
};

template<typename T>
struct RemoveReference<T&&>
{
 typedef T Type;
};

template<typename T>
constexpr T&& ForwardValue(typename RemoveReference<T>::Type&& value)
{
 return static_cast<T&&>(value);
}

template<typename T>
constexpr T&& ForwardValue(typename RemoveReference<T>::Type& value)
{
 return static_cast<T&&>(value);
}

其中函数模板ForwardValue就是对std::forward的简单实现。

2.4关于引用折叠

C++11中实现完美转发依靠的是模板类型推导和引用折叠。模板类型推导比较简单,STL中的容器广泛使用了类型推导。比如,当转发函数的实参是类型X的一个左值引用,那么模板参数被推导为X&,当转发函数的实参是类型X的一个右值引用的话,那么模板的参数被推导为X&&类型。再结合引用折叠规则,就能确定出参数的实际类型。

引用折叠式什么?引用折叠规则就是左值引用与右值引用相互转化时会发生类型的变化,变化规则为:

1. T& + & => T&
2. T&& + & => T&
3. T& + && => T&
4. T&& + && => T&&

上面的规则中,前者代表接受类型,后者代表进入类型,=>表示引用折叠之后的类型,即最后被推导决断的类型。简单总结为:
 (1)所有右值引用折叠到右值引用上仍然是一个右值引用;
 (2)所有的其他引用类型之间的折叠都将变成左值引用。

通过引用折叠规则保留参数原始类型,完美转发在不破坏const属性的前提下,将参数完美转发到目的函数中。

3.右值引用的作用

右值引用的作用是用于移动构造函数(Move Constructors)和移动赋值运算符( Move Assignment Operator)。为了让我们自己定义的类型支持移动操作,我们需要为其定义移动构造函数和移动赋值运算符。这两个成员类似对应的拷贝操作,即拷贝构造和赋值运算符,但它们从给定对象窃取资源而不是拷贝资源。

移动构造函数:

移动构造函数类似于拷贝构造函数,第一个参数是该类类型的一个右值引用,同拷贝构造函数一样,任何额外的参数都必须有默认实参。完成资源移动后,原对象不再保留资源,但移动构造函数还必须确保原对象处于可销毁的状态。

移动构造函数的相对于拷贝构造函数的优点:移动构造函数不会因拷贝资源而分配内存,仅仅接管源对象的资源,提高了效率。

移动赋值运算符:

移动赋值运算符类似于赋值运算符,进行的是资源的移动操作而不是拷贝操作从而提高了程序的性能,其接收的参数也是一个类对象的右值引用。移动赋值运算符必须正确处理自赋值。

下面给出移动构造函数和移动析构函数利用右值引用来提升程序效率的实例,首先我先写了一个山寨的vector:

#include <iostream>
#include <string>
using namespace std;

class Obj
{
public:
 Obj(){cout <<"create obj" << endl;}
 Obj(const Obj& other){cout<<"copy create obj"<<endl;}
};

template <class T> class Container
{
public:
  T* value;
public:
  Container() : value(NULL) {};
  ~Container()
  {
 if(value) delete value;
 }

 //拷贝构造函数
 Container(const Container& other)
 {
    value = new T(*other.value);
 cout<<"in constructor"<<endl;
  }
 //移动构造函数
  Container(Container&& other)
  {
 if(value!=other.value){
  value = other.value;
  other.value = NULL;
 }
 cout<<"in move constructor"<<endl;
  }
 //赋值运算符
  const Container& operator = (const Container& rhs)
  {
 if(value!=rhs.value)
 {
  delete value;
  value = new T(*rhs.value);
 }
 cout<<"in assignment operator"<<endl;
    return *this;
  }
 //移动赋值运算符
  const Container& operator = (Container&& rhs)
  {
 if(value!=rhs.value)
 {
  delete value;
  value=rhs.value;
  rhs.value=NULL;
 }
 cout<<"in move assignment operator"<<endl;
    return *this;
  }

  void push_back(const T& item)
  {
    delete value;
    value = new T(item);
  }
};

Container<Obj> foo()
{
 Container<Obj> c;
   c.push_back(Obj());
 cout << "---- exit foo ----" << endl;
   return c;
}

int main()
{
 Container<Obj> v;
 v=foo(); //采用移动构造函数来构造临时对象,再将临时对象采用移动赋值运算符移交给v
 getchar();
}

程序输出:

create obj
copy create obj
---- exit foo ----
in move constructor
in move assignment operator

上面构造的容器只能存放一个元素,但是不妨碍演示。从函数foo中返回容器对象全程采用移动构造函数和移动赋值运算符,所以没有出现元素的拷贝情况,提高了程序效率。如果去掉Container的移动构造函数和移动赋值运算符,程序结果如下:

create obj
copy create obj
---- exit foo ----
copy create obj
in constructor
copy create obj
in assignment operator

可见在构造容器Container的临时对象tmp时发生了元素的拷贝,然后由临时对象tmp再赋值给v时,又发生了一次元素的拷贝,结果出现了无谓的两次元素拷贝,这严重降低了程序的性能。由此可见,右值引用通过移动构造函数和移动赋值运算符来实现对象移动在C++程序开发中的重要性。

同理,如果想以左值来调用移动构造函数构造容器Container的话,那么需要将左值对象通过std::move来获取对其的右值引用,参考如下代码:

//紧接上面的main函数中的内容
Container<Obj> c=v;  //调用普通拷贝构造函数,发生元素拷贝
cout<<"-------------------"<<endl;
Container<Obj> c1=std::move(v); //获取对v的右值引用,然后调用移动构造函数构造c1
cout<<c1.value<<endl;
cout<<v.value<<endl;   //v的元素值已经在动构造函数中被置空(被移除)

代码输出:

copy create obj
in constructor
-------------------
in move constructor
00109598
00000000

以上就是详解c++11 移动语义与右值引用的详细内容,更多关于c++11 移动语义与右值引用的资料请关注我们其它相关文章!

(0)

相关推荐

  • C++11的右值引用的具体使用

    C++11 引入了 std::move 语义.右值引用.移动构造和完美转发这些特性.由于这部分篇幅比较长,分为3篇来进行阐述. 在了解这些特性之前,我们先来引入一些问题. 一.问题导入 函数返回值是传值的时候发生几次对象构造.几次拷贝? 函数的形参是值传递的时候发生几次对象构造? 让我们先来看一段代码, // main.cpp #include <iostream> using namespace std; class A{ public: A(){ cout<<"cla

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

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

  • C++11 模板参数的“右值引用”是转发引用吗

    在C++11中,&&不再只有逻辑与的含义,还可能是右值引用: void f(int&& i); 但也不尽然,&&还可能是转发引用: template<typename T> void g(T&& obj); "转发引用"(forwarding reference)旧称"通用引用"(universal reference),它的"通用"之处在于你可以拿一个左值绑定给转发引用

  • C++11中value category(值类别)及move semantics(移动语义)的介绍

    前言 C++11之前value categories只有两类,lvalue和rvalue,在C++11之后出现了新的value categories,即prvalue, glvalue, xvalue.不理解value categories可能会让我们遇到一些坑时不知怎么去修改,所以理解value categories对于写C++的人来说是比较重要的.而理解value categories离不开一个概念--move semantics.了解C++11的人我相信都了解了std::move,右值引用

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

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

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

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

  • C++ lambda 捕获模式与右值引用的使用

    lambda 表达式和右值引用是 C++11 的两个非常有用的特性. lambda 表达式实际上会由编译器创建一个 std::function 对象,以值的方式捕获的变量则会由编译器复制一份,在 std::function 对象中创建一个对应的类型相同的 const 成员变量,如下面的这段代码: int main(){ std::string str = "test"; printf("String address %p in main, str %s\n", &a

  • 深入了解c++11 移动语义与右值引用

    1.移动语义 C++11新标准中一个最主要的特性就是提供了移动而非拷贝对象的能力.如此做的好处就是,在某些情况下,对象拷贝后就立即被销毁了,此时如果移动而非拷贝对象会大幅提升性能.参考如下程序: //moveobj.cpp #include <iostream> #include <vector> using namespace std; class Obj { public: Obj(){cout <<"create obj" << e

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

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

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

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

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

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

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

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

  • C++左值与右值,右值引用,移动语义与完美转发详解

    目录 C++——左值与右值.右值引用.移动语义与完美转发 一.左值和右值的定义 二.如何判断一个表达式是左值还是右值(大多数场景) 三.C++右值引用 四.std::move()与移动语义 五. 完美转发 总结 C++——左值与右值.右值引用.移动语义与完美转发 在C++或者C语言中,一个表达式(可以是字面量.变量.对象.函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式. 一.左值和右值的定义 1.左值的英文为locator value,简写为lvalue,可意为存储在内存中.有明

随机推荐