C++ 11 中的右值引用
右值引用的功能
首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能:
#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> k;
k = foo();
}
首先我们编译一下这个函数,运行结果如下:
tianfang > g++ main.cpp
tianfang > a.out
>> create obj
>> copy create obj
---- exit foo ----
>> copy create obj
tianfang >
可以看到,对obj对象执行了两次构造。vector是一个常用的容器了,我们可以很容易的分析这这两次拷贝构造的时机:
- foo函数第二行,调用push_back的时候,会在vector里建立一个obj的副本
- main函数第二行,执行复制函数的时候,会把foo()返回的对象全部复制过来,再次执行一次拷贝构造
由于对象的拷贝构造的开销是非常大的,因此我们想就可能避免他们。其中,第一次拷贝构造是vector的特性所决定的,不可避免。但第二次拷贝构造,在C++ 11中就是可以避免的了。
tianfang > g++ -std=c++11 main.cpp
tianfang > a.out
>> create obj
>> copy create obj
---- exit foo ----
tianfang >
可以看到,我们除了加上了一个-std=c++11选项外,什么都没干,但现在就把第二次的拷贝构造给去掉了。它是如何实现这一过程的呢?
在老版本中,当我们执行第二行的赋值操作的时候,执行过程如下:
- foo()函数返回一个临时对象(这里用~tmp来标识它)
- 执行vector的 '=' 函数,将对象k中的现有成员删除,将~tmp的成员复制到k中来
- 删除临时对象~tmp
在C++11的版本中,执行过程如下:
- foo()函数返回一个临时对象(这里用~tmp来标识它)
- 执行vector的 '=' 函数,将对象k中的成员~tmp的成员互换,此时k中的成员就被替换成了~tmp中的成员。
- 删除临时对象~tmp(此时就删除了以前的k中的成员)
关键的过程就是第2步,它不是复制而是交换,从而避免的成员的拷贝,但效果却是一样的。不用修改代码,性能却得到了提升,对于程序员来说就是一份免费的午餐。
但是,这份免费的午餐也不是无条件就可以获取的,带上-std=c++11编译时,如果使用STL代码可以享用这份午餐,但如果使用我们以前的老代码发现还是和以前的功能是一样的,那么,如何让我们以前的代码也能得到这个效率的提升呢?
通过交换减少数据的拷贝
为了演示如何在我们的代码中也获取这个性能提升,首先我先写了一个山寨的vector:
#include <iostream>
#include <vector>
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() { delete value; }
container(const container& other)
{
value = new T(*other.value);
}
const container& operator = (const container& other)
{
delete value;
value = new T(*other.value);
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> k ;
k = foo();
}
这个vector只能容纳一个元素,但并不妨碍我们的演示,其功能和前面的例子是一样的,运行这段代码,结果如下:
tianfang > make
g++ -std=c++11 main.cpp
tianfang > a.out
>> create obj
>> copy create obj
---- exit foo ----
>> copy create obj
tianfang >
如前所述,仍然有两次拷贝构造。其实前面已经说过交换实现减少拷贝构造的原理,那么,我们可以通过修改 '=' 函数来手动实现这一过程。
const container& operator = (container& other)
{
T* tmp = value;
value = other.value;
other.value = tmp;
return *this;
}
在VC中运行这段代码,发现运行结果和预期一致,
>> create obj
>> copy create obj
---- exit foo ----
但是,gcc中却无法通过编译,原因很简单:gcc期望的赋值函数的参数是const型的,而这里为了交换成员,而不能使用const型。
那么,虽然gcc中不能生效,是否可以说在vc中就可以以这种形式获取性能提升呢?答案是否定的。虽然在这段代码中这么写没有问题,但赋值函数本身是期望复制功能的,而不是交换。例如,修改后下面的运行结果就不对了。
int main()
{
container<obj> k, k2;
k = foo();
//预期结果是复制,但执行了交换
k2 = k;
}
gcc的告警是有道理的:如果 '=' 函数实现的是复制功能,虽然效率低点,但保证了功能正确,但如果实现的是交换的功能,则不能保证功能一定正确。只有当 '=' 函数右边的对象为一个临时变量的时候,由于临时变量会马上被删除掉,此时的交换和复制的效果是一样的。其实VC也应该把这个告警加上才合适。
PS:对临时变量定义和来源不清楚的朋友可以参考一下这篇文章。
现在的问题是:我们无法在赋值函数里区分传入的是一个临时对象还是非临时对象,因此只能执行复制操作。为了解决这一问题,c++中引入了一个新的赋值函数的重载形式:
container& operator = (container&& other)
这个赋值函数通常称为移动赋值函数,和老版本的相比,它有两点区别:
- 入参不是const型,因此它是可以更改入参的值的,从而实现交换操作
- 入参前面有两个&号,这个是C++11引入的新语法,称为右值引用,它的使用方式和普通引用是一样的,唯一的区别是可以指向临时变量。
现在,我们就有两个版本的赋值函数了,C++11在语法级别也做了适应:
- 如果入参是临时变量,则执行移动赋值函数,如果没有定义移动赋值函数,则执行复制赋值函数(以保证老版本代码能编译通过)
- 如果入参不是临时变量,则执行普通的复制赋值函数
现在,我们实现一下山寨版的移动赋值函数:
container& operator = (container&& other)
{
delete value;
value = other.value;
other.value = NULL;
return *this;
}
运行后结果就和我们期望的那样,避免了成员的第二次的拷贝构造。
和移动赋值函数相应的,也有一个一个移动构造函数,也最好实现以下:
container (container&& other)
{
value = other.value;
other.value = NULL;
}
我们也可以实现自己的右值引用版的重载函数,这里就不多介绍了。
注意:本文所示的代码只是为了演示和实现右值引用,力求简洁,并没有写得很完善(一个典型的缺失就是在赋值函数中没有判断入参是否是本身),请不要将其应用于项目中。
完善的版本请看MSDN文章:如何编写一个移动构造函数,其相应的对右值引用的介绍文章Rvalue引用声明:&&也非常值得一读。
通过std::move函数显式使用交换
首先看一下这段代码:
class bigobj
{
public :
bigobj() { cout << ">> create obj " << endl; }
bigobj(const bigobj& other) { cout << ">> copy create obj " << endl; }
bigobj(bigobj&& other) { cout << ">> move create obj " << endl; }
};
int main()
{
list<bigobj> list;
for(int i = 0; i < 3; i++)
{
bigobj obj;
list.push_back(obj);
}
}
运行的时候就会发现:虽然我们定义了移动构造函数,但是它仍然会执行拷贝构造函数。这是因为编译器并不认为obj是临时变量。关于什么变量才是临时变量,前文已经给了个链接来说明它,简单的说,我们能够看到的命名变量都不是临时变量。
虽然obj对象不是语言级别的临时变量,但是从功能上来看,它就是一个临时变量,是可以使用移动构造函数来消除拷贝带来的性能损失的。为了解决这一问题,C++提供了一个move函数来把obj变量强制转换为右值引用,这样就可以使用移动构造函数了。
for(int i = 0; i < 3; i++)
{
bigobj obj;
list.push_back(std::move(obj));
}
不过,需要注意的是,和系统识别的临时变量而自动使用右值引用不同,这种强制转换是有一定的风险的,由于在push_back后执行了交换操作,如果再次使用它会出现非预期的结果,只有能确定该变量不会再次被使用才能执行这种转换。
C++ 11 中的右值引用的更多相关文章
- [转载] C++11中的右值引用
C++11中的右值引用 May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导 ...
- C++11中的右值引用
原文出处:http://kuring.me/post/cpp11_right_reference May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移 ...
- C++ 11中的右值引用以及std::move
看了很多篇文章,现在终于搞懂了C++ 中的右值以及std::move 左值和右值最重要的区别就是右值其实是一个临时的变量 在C++ 11中,也为右值引用增加了新语法,即&& 比 ...
- 【转载】C++ 11中的右值引用
本篇随笔为转载,原博地址如下:http://www.cnblogs.com/TianFang/archive/2013/01/26/2878356.html 右值引用的功能 首先,我并不介绍什么是右值 ...
- C++11中的右值引用及move语义编程
C++0x中加入了右值引用,和move函数.右值引用出现之前我们只能用const引用来关联临时对象(右值)(造孽的VS可以用非const引用关联临时对象,请忽略VS),所以我们不能修临时对象的内容,右 ...
- C++11标准之右值引用(rvalue reference)
1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了Copy Elision.RVO(包 ...
- 谈谈 C++ 中的右值引用
转自:https://liam0205.me/2016/12/11/rvalue-reference-in-Cpp/ 最近在改 XGBoost 的代码.XGBoost 在代码中使用了很多来自 C++1 ...
- 对C++11中的`移动语义`与`右值引用`的介绍与讨论
本文主要介绍了C++11中的移动语义与右值引用, 并且对其中的一些坑做了深入的讨论. 在正式介绍这部分内容之前, 我们先介绍一下rule of three/five原则, 与copy-and-swap ...
- c++ 11 移动语义、std::move 左值、右值、将亡值、纯右值、右值引用
为什么要用移动语义 先看看下面的代码 // rvalue_reference.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #includ ...
随机推荐
- eclipse中去除build时总是js错误的问题
在用eclipse时经常莫名其名的弹出如下框框,有的时候甚至还死循环了.严重影响开发效率. 原因分析就是我们项目的一些js代码,eclipse验证时有错误的,其实是没有错误的.不知道eclipse是怎 ...
- NSLogger 简单用法总结
NSLogger 支持在同一个本地网络下,移动 App产生的日志,通过 Bonjour 网络传送到电脑上查看日志信息. 1.具体用法: 在移动App项目里,添加3个文件: LoggerCommon.h ...
- java设计模式之原型模式
原型模式概念 该模式的思想就是将一个对象作为原型,对其进行复制.克隆,产生一个和原对象类似的新对象.java中复制通过clone()实现的.clone中涉及深.浅复制.深.浅复制的概念如下: ⑴浅复制 ...
- 6.bootstrap练习笔记-缩略图和list-group
bootstrap练习笔记-缩略图 1.其实缩略图很简单,只要按照固定的格式来设计 div.container 总容器 在宽度为1200px以上 div.row 一行内容 div.col-lg-3. ...
- 用于部署war并重启Tomcat的脚本
只需要定义两个变量, 一个是目标tomcat实例的目录, 另一个是war包的名称 # Please define the absolute path of tomcat instance THIS_T ...
- CXF 动态创建客户端调用稳定版本号为2.7.18
今天用动态创建客户端的方式调用webservice,报了这样一个错: 2017-01-05 20:51:46,029 DEBUG main org.apache.cxf.common.logging. ...
- rpc框架之HA/负载均衡构架设计
thrift.avro.grpc之类的rpc框架默认都没有提供负载均衡的实现,生产环境中如果server只有一台,显然不靠谱,于是有了下面的设计,这其实是前一阵跟北京一个朋友在qq群里交流的结果,分享 ...
- 十分钟轻松让你认识ASP.NET MVC6
这篇文章说明下如何在普通编辑器下面开发mvc6应用程序. 上篇文章: 十分钟轻松让你认识ASP.NET 5(MVC6) 首先安装mvc6的nuget包: 可以看到在project.json文件中添加了 ...
- 踩坑所引发出的appendChild方法的介绍
问题描述 最近在做项目时,遇到一个问题,当js生成一个组件后,会注入到页面的某个节点里显示.在组件内部进行了一次注入操作,在调用组件的外部js文件中也进行了一次注入操作,结果发现页面里只生成了一份组件 ...
- Macbook被格式化之后
macbook不小心被手贱格式化了,开机显示一个大问号. 于是查询得到恢复方式是使用command+R. 照做了,试了好几次,那个地球还是卡住不动的.都没有提示让我输入wifi密码. 于是又查了一下, ...