C++右值引用浅析
一直想试着把自己理解和学习到的右值引用相关的技术细节整理并分享出来,希望能够对感兴趣的朋友提供帮助。
右值引用是C++11标准中新增的一个特性。右值引用允许程序员可以忽略逻辑上不需要的拷贝;而且还可以用来支持实现完美转发的函数。它们都是实现更高效、更健壮的库。
move语义
先不展开具体右值引用定义。先说说move语义。右值引用是用来支持move语义的。move语义是指将一个同类型的对象A中的资源(可能是在堆上分配,也可能是一个文件句柄或者其他系统资源)搬移到另一个同类型的对象B中,解除对象A对该资源的所有权。这样可以减少不必要的临时对象的构造、拷贝以及析构等动作。比如我们经常使用的std::vector<T>,当两个相同的std::vector类型赋值时,一般的步骤如下:
- 内部的赋值构造函数一般是先分配指定大小的内存,
- 从源std::vector中拷贝到新申请的内存,
- 之后再把原有的对象实例析构掉,
- 最后接管新申请的数据。
这就是我们C++11之前使用的拷贝语义,也就是常说的深拷贝。move语义与拷贝语义相对,类似于浅拷贝,但是资源的所有权发生了转移。move语义的实现可以减少拷贝动作,大幅提高程序的性能。
而为了实现move语义的构造,就需要对应的语法来支持。原有的拷贝构造函数等不能够满足该需求。最典型的例子就是C++11废弃的std::auto_ptr,其构造函数会产生不明确的拥有权关系,很容易滋生BUG。这也是很多人不喜欢std::auto_ptr的原因。C++11为此增加了相应的构造函数。
class Foo {
public:
Foo(Foo&& f) {}
Foo& operator=(Foo&& f) {
return *this;
}
};
这里可以明显看到两个函数中的参数类型是Foo&&。这就是右值引用的基本语法。这样做的目的是通过函数重载实现不同的功能处理。
强制move语义
C++11规定即可以在右值上使用move语义,也可以在左值上使用move语义。也就是说,可以把一个左值转为右值引用,然后使用move语义。比如在C++的经典函数swap中:
template<class T>
void swap(T& a, T& b)
{
T tmp(a);
a = b;
b = tmp;
} X a, b;
swap(a, b);
上面代码中没有右值,但是tmp变量只作用在本函数作用域中,只是用来承担数据的转移动作。C++11制定的上述规则在这里反而可以得到非常好的适用。C++11为了达到这个规则,实现了std::move函数,这个函数的就是把传入的参数转换为一个右值引用并返回。也就是说在C++11下,swap的实现如下:
template<class T>
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
} X a, b;
swap(a, b);
我们在实际使用中,也可以尽量的多使用std::move。只要求我们自定义的类型实现转移构造函数。
右值引用
为了说清楚右值引用什么,就不得不说左值和右值。简单的说左值是一个指向某内存空间的表达式,并且我们可以用&操作符获得该内存空间的地址。右值就是非左值的表达式。可以阅读这篇《Lvalues and Rvalues》进行深入理解。
右值引用非常类似于C++的普通引用,也是一个复合类型。为了方便区分,普通引用就是左值引用。一个左值引用就是在类型后面加&操作符。而右值引用就是在类型后加&&操作符,就像上面的转移构造函数的参数一样。
右值引用的行为类似于左值引用,但是右值引用只能绑定临时对象,不能绑定一个左值引用。右值引用的出现还影响了函数重载决议。左值会优先适配左值引用参数的函数,右值会优先适配右值引用参数的函数:
void foo(X& x); // lvalue reference overload
void foo(X&& x); // rvalue reference overload X x;
X foobar(); foo(x); // argument is lvalue: calls foo(X&)
foo(foobar()); // argument is rvalue: calls foo(X&&)
理论上,你可以用这种方式重载任何函数,但是绝大多数情况下这样的重载只出现在拷贝构造函数和赋值运算符中,也就是实现move语义。
如果你实现了void foo(X&);,但是没有实现void foo(X&&);,那么和以前一样foo的参数只能是左值。如果实现了void foo(X const &);,但是没有实现void foo(X&&);,仍和以前一样,foo的参数既可以是左值也可以是右值。唯一能够区分左值和右值的办法就是实现void foo(X&&);。最后,如果只实现了实现void foo(X&&);,但却没有实现void foo(X&);和void foo(X const &);,那么foo的参数将只能是右值。
右值引用是右值吗?
void foo(X&& x)
{
X anotherX = x;
// ...
}
在上面这个函数foo内,X的哪个构造函数会被调用?是拷贝构造还是转移构造?按照我们之前说的,这是个右值引用,应该是调用的X(X&&);函数。但是实际上,这里调用的是X(const X&);这里就是让人迷惑的地方:右值引用类型既可以被当做左值也可以被当做右值,判断的标准是该右值引用是否有名字。有名字就是左值,否则就是右值。如果要做到把带有名字的右值引用变为右值,就需要借助std::move函数。
void foo(X&& x)
{
X anotherX = std::move(x);
// ...
}
在实现自己的转移构造函数时,一些人没有理解这一点,导致在自己的转移构造函数内部的实现中实际是执行了拷贝构造函数。
move语义与返回值优化
了解了move语义和强制move以及右值引用的一些概念后,有些朋友在实现一些函数时,会在返回的地方进行强制move。认为这样可以减少一次拷贝。比如:
X foo()
{
X x;
// perhaps do something to x
return std::move(x); // making it worse!
}
实际上这种是不需要的。因为编译器会做返回值优化(Return Value Optimization)。在C++11标准中有如下规定:
When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.
直接return x;是NRVO支持的一种用例场景,可以做到多余的拷贝构造。编译器会自己选择使用拷贝构造还是move构造函数。
但是如果用std::move(x);那么可能会带来额外的影响:可能会阻止NRVO。也就是说可能需要额外的开销来执行move语义。
建议阅读Stackoverflow上的这两个问题:
《When should std::move be used on a function return value?》
《Why does std::move prevent RVO?》
完美转发
右值引用除了用来实现move语义之外,还就是为了解决完美转发的问题。我们有的时候会写工厂函数,比如如下代码:
template<typename T, typename Arg>
shared_ptr<T> factory(Arg arg)
{
return shared_ptr<T>(new T(arg));
}
这个实现非常简单,就是把参数arg传给类T进行构造。但是这里引入了额外的通过值的函数调用,不使用于那些以引用为参数的构造函数。
那么为了解决这个问题,就有人想到用引用,比如:
template<typename T, typename Arg>
shared_ptr<T> factory(Arg& arg)
{
return shared_ptr<T>(new T(arg));
}
但是这里又有问题,不能接收右值作为参数。
factory<X>(hoo()); // error if hoo returns by value
factory<X>(); // error
对应的解决办法是继续引入const引用。如果有多个参数的情况下,这个函数的参数列表就变的比较恶心了。同时还有个问题就是不能实现move语义。
而右值引用可以解决这个问题,可以不用通过重载函数来实现真正的完美转发。但是它需要配合两个右值引用的规则:
- 引用叠加规则
A& & => A&
A& && => A&
A&& & => A&
A&& && => A&&
- 模板参数推导规则
template<typename T>
void foo(T&&);
当函数foo的实参是一个A类型的左值时,T的类型是A&。再根据引用叠加规则判断,最后参数的实际类型是A&。
当foo的实参是一个A类型的右值时,T的类型是A。根据引用叠加规则可以判断,最后的类型是A&&。
有了上面这些规则,我们可以用右值引用来解决前面的完美转发问题。下面是解决的办法:
template<typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg)
{
return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
而std::forward的实现如下:
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
return static_cast<S&&>(a);
}
这里就不展开具体的例子来说明了,明白了上述的两个规则就可以明白了。建议阅读Scott Meyers的《Universal References in C++11》.
参考文档
- 《C++ Rvalue References Explain》
- 《Universal References in C++11》
- 《A Brief Introduction to Rvalue References》
总结
右值引用的出现虽然看似增加了额外的复杂度,但是它带来的收益还是非常明显的,能够帮助实现move语义,提升程序的性能;又可以实现完美转发,方便了库的设计。
C++就是这样,给你一个新增一个特性之后,也会带来额外的学习难度。但是这也是很多人喜欢C++的原因,它给了程序员太多的可能性。可以精准的控制对象的生命周期,是高性能程序必不可少的工具。
C++右值引用浅析的更多相关文章
- C++ 11 中的右值引用
C++ 11 中的右值引用 右值引用的功能 首先,我并不介绍什么是右值引用,而是以一个例子里来介绍一下右值引用的功能: #include <iostream> #include &l ...
- 图说函数模板右值引用参数(T&&)类型推导规则(C++11)
见下图: 规律总结: 只要我们传递一个基本类型是A④的左值,那么,传递后,T的类型就是A&,形参在函数体中的类型就是A&. 只要我们传递一个基本类型是A的右值,那么,传递后,T的类型就 ...
- c++11的右值引用、移动语义
对于c++11来说移动语义是一个重要的概念,一直以来我对这个概念都似懂非懂.最近翻翻资料感觉突然开窍,因此记下.其实搞懂之后就会发现这个概念很简单,并无什么高深的地方. 先说说右值引用.右值一般指的是 ...
- VS2012 error C2664: “std::make_pair”:无法将左值绑定到右值引用
在vs2012(c++)make_pair()改动: C++: template <class T1, class T2> pair<V1, V2> make_pair(T1& ...
- 右值引用、move与move constructor
http://blog.chinaunix.net/uid-20726254-id-3486721.htm 这个绝对是新增的top特性,篇幅非常多.看着就有点费劲,总结更费劲. 原来的标准当中,参数与 ...
- 【转】C++11 标准新特性: 右值引用与转移语义
VS2013出来了,对于C++来说,最大的改变莫过于对于C++11新特性的支持,在网上搜了一下C++11的介绍,发现这篇文章非常不错,分享给大家同时自己作为存档. 原文地址:http://www.ib ...
- move语义和右值引用
C++11支持move语义,用以避免非必要拷贝和临时对象. 具体内容见收藏中的“C++右值引用” .
- [转载] C++11中的右值引用
C++11中的右值引用 May 18, 2015 移动构造函数 C++98中的左值和右值 C++11右值引用和移动语义 强制移动语义std::move() 右值引用和右值的关系 完美转发 引用折叠推导 ...
- C++ 11 右值引用
C++11中引入的一个非常重要的概念就是右值引用.理解右值引用是学习“移动语义”(move semantics)的基础.而要理解右值引用,就必须先区分左值与右值. 注意:左值右值翻译可能有些问题 *L ...
随机推荐
- In-Memory:在内存中创建临时表和表变量
在Disk-Base数据库中,由于临时表和表变量的数据存储在tempdb中,如果系统频繁地创建和更新临时表和表变量,大量的IO操作集中在tempdb中,tempdb很可能成为系统性能的瓶颈.在SQL ...
- MIP改造常见问题二十问
在MIP推出后,我们收到了很多站长的疑问和顾虑.我们将所有疑问和顾虑归纳为以下二十个问题,希望对大家理解 MIP 有帮助. 1.MIP 化后对其他搜索引擎抓取收录以及 SEO 的影响如何? 答:在原页 ...
- ElasticSearch 5学习(9)——映射和分析(string类型废弃)
在ElasticSearch中,存入文档的内容类似于传统数据每个字段一样,都会有一个指定的属性,为了能够把日期字段处理成日期,把数字字段处理成数字,把字符串字段处理成字符串值,Elasticsearc ...
- 前端学HTTP之字符集
前面的话 HTTP报文中可以承载以任何语言表示的内容,就像它能承载图像.影片或任何类型的媒体那样.对HTTP来说,实体主体只是二进制信息的容器而已.为了支持国际性内容,服务器需要告知客户端每个文档的字 ...
- jQuery动画-圣诞节礼物
▓▓▓▓▓▓ 大致介绍 下午看到了一个送圣诞礼物的小动画,正好要快到圣诞节了,就动手模仿并改进了一些小问题 原地址:花式轮播----圣诞礼物传送 思路:动画中一共有五个礼物,他们平均分布在屏幕中,设置 ...
- Win.ini和注册表的读取写入
最近在做打包的工作,应用程序的配置信息可以放在注册表文件中,但是在以前的16位操作系统下,配置信息放在Win.ini文件中.下面介绍一下Win.ini文件的读写方法和注册表的编程. 先介绍下Win.i ...
- PhpStorm和WAMP配置调试参数,问题描述Error. Interpreter is not specified or invalid. Press “Fix” to edit your project configuration.
PhpStorm和WAMP配置调试参数 问题描述: Error. Interpreter is not specified or invalid. Press “Fix” to edit your p ...
- 项目管理_FindBugs的使用
本章将讲述如何在Myeclipse下,使用FindBugs,静态分析工具,无需开发人员费劲就能找出代码中已有的缺陷. 一:Myeclipse下如何安装FindBugs插件 1:FindBugs插件下载 ...
- hbase协处理器编码实例
Observer协处理器通常在一个特定的事件(诸如Get或Put)之前或之后发生,相当于RDBMS中的触发器.Endpoint协处理器则类似于RDBMS中的存储过程,因为它可以让你在RegionSer ...
- 如何让spring mvc web应用启动时就执行特定处理
Asp.Net的应用中通过根目录下的Global.asax,在Application_Start方法中做一些初始化操作,比如:预先加载缓存项对网站热点数据进行预热,获取一些远程的配置信息等等. Spr ...