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” 编译选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。一个 “-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)临时对象无其他用户。

这两个特性意味着,使用右值引用的代码可以自由地接管所引用的对象的资源。关于无名临时对象,请参见认识C++中的临时对象temporary object。

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 referencetypedef _Ty type;
};template<class _Ty> struct remove_reference<_Ty&>{    // remove referencetypedef _Ty type;
};template<class _Ty> struct remove_reference<_Ty&&>{        // remove rvalue referencetypedef _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);        //输出rvalueint 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();   //采用移动构造函数来构造临时对象,再将临时对象采用移动赋值运算符移交给vgetchar();
}

程序输出:

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

参考文献

[1] Stanley B. Lippman著,王刚 杨巨峰译.C++ Primer中文版第五版.2013:470-485
[2] C++ 11 中的右值引用
[3] C++中typename关键字的使用方法和注意事项
[4] 深入理解C++11[M].3.3右值引用:移动语义和完美转发
[5](原创)C++11改进我们的程序之move和完美转发
[6] 详解C++11中移动语义(std::move)和完美转发(std::forward)

C++11 移动语义与右值引用相关推荐

  1. C++11 标准新特性: 右值引用与转移语义(点评)

    <<C++11 标准新特性: 右值引用与转移语义>> 原文地址如下 http://www.ibm.com/developerworks/cn/aix/library/1307_ ...

  2. C++11新特性之右值引用

    什么是对象? An object is "something in memory". 什么是左值,什么是右值? An lvalue expression identifies a ...

  3. std::move C++11 标准新特性: 右值引用与转移语义

    新特性的目的 右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和 ...

  4. C++11 标准新特性: 右值引用与转移语义

    原文地址 http://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/ C++ 的新标准 C++11 已经发布一段时间了.本文介绍了新 ...

  5. C++11新特性——移动语义,右值引用

    移动语义 有一些类的资源是__不可共享__的,这种类型的对象可以被移动但不能被拷贝,如:IO 或 unique_ptr 库容器.string 和 shared_ptr 支持拷贝和移动,IO 和 uni ...

  6. (译)C++11中的Move语义和右值引用

    郑重声明:本文是笔者网上翻译原文,部分有做添加说明,所有权归原文作者! 地址:http://www.cprogramming.com/c++11/rvalue-references-and-move- ...

  7. 【转】C++11 标准新特性: 右值引用与转移语义

    VS2013出来了,对于C++来说,最大的改变莫过于对于C++11新特性的支持,在网上搜了一下C++11的介绍,发现这篇文章非常不错,分享给大家同时自己作为存档. 原文地址:http://www.ib ...

  8. 虚幻引擎与现代C++:转移语义和右值引用

    文章转自微信公众号 "游戏程序员的自我修养",作者房燕良 所谓的"现代C++",就是指C++ 11标准之后的C++语言,与之相对应的是"经典C++&q ...

  9. 虚幻4与现代C++:转移语义和右值引用

    所谓的"现代C++",就是指C++ 11标准之后的C++语言,与之相对应的是"经典C++",也就是C++ 98/03标准的C++语言.Unreal Engine ...

最新文章

  1. 英文构词法 —— ant、ent 后缀
  2. elementui树形复选框_Element-ui表格树形控件结合复选框实践
  3. android Ant批打包学习(零)--基础知识
  4. k8s pod内部容器_第三章 pod:运行于kubernetes中的容器
  5. python爬取appstore的评论数据的步骤_python数据抓取分析
  6. db2与mysql语法区别,db2和mysql语法分析异同点
  7. 软件开发工具--自考2018年10月程序填空
  8. Python的apidoc操作
  9. 正确使用日志的10个技巧(转)
  10. linux-inject:注入代码到运行的Linux进程中
  11. pdf转cad格式工具控件pdf2cad
  12. 微信小程序 tab点击切换(不滑动)
  13. 重学 Java 设计模式:实战享元模式「基于Redis秒杀,提供活动与库存信息查询场景」
  14. android动态壁纸的制作教程,android – 动态壁纸教程
  15. 投身开源,需要持之以恒的热爱与贡献 —— Apache Spark Committer 姜逸坤
  16. 如何用USB启动系统
  17. 工作中使用到的单词(软件开发)_2021-12-26_备份
  18. centos7 安装oracle的问题
  19. 【标准全文】GB 38031-2020 电动汽车用动力蓄电池安全要求
  20. 机器语言对不同型号的计算机来说一般是不同的

热门文章

  1. 微软发布 Autodesk FBX 漏洞带外安全公告,将于5月推出补丁
  2. 看我如何发现开源 WAF引擎ModSecurity 中的DoS 漏洞
  3. lvs之 lvs+nginx+tomcat_1、tomcat_2+redis(lvs dr 模式)
  4. Win10 UWP开发系列:开发一个自定义控件——带数字徽章的AppBarButton
  5. ArcGIS 导出点图层的中的XY坐标
  6. WiMAX版图不止3G
  7. C中的预编译宏定义-转
  8. [Python] L1-047 装睡-PAT团体程序设计天梯赛GPLT
  9. 蓝桥杯 ADV-74 算法提高 计算整数因子
  10. Python出现quot; SyntaxError: Non-ASCII character '\xe6' 或'\xd6' in filequot;错误解决方法