[C++]C++11右值引用
右值引用的概念(摘自C++Primer)
左值和右值的概念
1、左值和右值是表达式的属性,一些表达式要求生成左值,一些表达式要求生成右值;左值表达式通常是一个对象的身份,而一个右值表达式表示的是对象的值。
2、左值持久,右值短暂,右值只能绑定到临时对象,所引用的对象即将销毁并且该对象没有其他用户,由此可知,使用右值引用的代码可以自由地接管所引用的对象的资源。
先看看移动语义
右值引用是C++11中最重要的新特性之一,它解决了C++中大量的历史遗留问题,使C++标准库的实现在多种场景下消除了不必要的额外开销(如std::vector, std::string),也使得另外一些标准库(如std::unique_ptr, std::function)(可以想象,如果没有右值引用,uniqueptr要如何实现资源所有权的转让?)成为可能。即使你并不直接使用右值引用,也可以通过标准库,间接从这一新特性中受益。
移动语义,简单来说解决的是各种情形下对象的资源所有权转移的问题。而在C++11之前,移动语义的缺失是C++饱受诟病的问题之一。举个栗子。问题一:如何将大象放入冰箱?答案是众所周知的。首先你需要有一台特殊的冰箱,这台冰箱是为了装下大象而制造的。你打开冰箱门,将大象放入冰箱,然后关上冰箱门。问题二:如何将大象从一台冰箱转移到另一台冰箱?普通解答:打开冰箱门,取出大象,关上冰箱门,打开另一台冰箱门,放进大象,关上冰箱门。2B解答:在第二个冰箱中启动量子复制系统,克隆一只完全相同的大象,然后启动高能激光将第一个冰箱内的大象气化消失。等等,这个2B解答听起来很耳熟,这不就是C++中要移动一个对象时所做的事情吗?“移动”,这是一个三岁小孩都明白的概念。将大象(资源)从一台冰箱(对象)移动到另一台冰箱,这个行为是如此自然,没有任何人会采用先复制大象,再销毁大象这样匪夷所思的方法。C++通过拷贝构造函数和拷贝赋值操作符为类设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式。否则,就需要自己实现移动资源的接口。为了实现移动语义,首先需要解决的问题是,如何标识对象的资源是可以被移动的呢?这种机制必须以一种最低开销的方式实现,并且对所有的类都有效。C++的设计者们注意到,大多数情况下,右值所包含的对象都是可以安全的被移动的。右值(相对应的还有左值)是从C语言设计时就有的概念,但因为其如此基础,也是一个最常被忽略的概念。不严格的来说,左值对应变量的存储位置,而右值对应变量的值本身。C++中右值可以被赋值给左值或者绑定到引用。类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。右值中的数据可以被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,需要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用可以被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,如果我们明确放弃对其资源的所有权,则可以通过std::move()来将其转为右值引用。std::move()实际上是static_cast
class People {
public:
People(string name) // 按值传入字符串,可接收左值、右值。接收左值时为复制,接收右值时为移动
: name_(move(name)) // 显式移动构造,将传入的字符串移入成员变量
{
}
string name_;
};
People a("Alice"); // 移动构造name
string bn = "Bob";
People b(bn); // 拷贝构造name
构造a时,调用了一次字符串的构造函数和一次字符串的移动构造函数。如果使用const string& name接收参数,那么会有一次构造函数和一次拷贝构造,以及一次non-trivial的析构。尽管看起来很蛋疼,尽管编译器还有优化,但从语义来说按值传入参数是最优的方式。如果你要在构造函数中接收std::shared_ptr并且存入类的成员(这是非常常见的),那么按值传入更是不二选择。拷贝std::shared_ptr需要线程同步,相比之下移动std::shared_ptr是非常轻松愉快的。按值返回和接收输入参数一样,返回值按值返回也是最符合人类思维的方式。曾经有无数函数为了返回容器而不得不写成这样void str_split(const string& s, vector* vec); // 一个按值语义定义的字符串拆分函数。这里不考虑分隔符,假定分隔符是固定的。
这样要求vec在外部被事先构造,此时尚无从得知vec的大小。即使函数内部有办法预测vec的大小,因为函数并不负责构造vec,很可能仍需要resize。对这样的函数嵌套调用更是痛苦的事情,谁用谁知道啊。有了移动语义,就可以写成这样
vector<string> str_split(const string& s) {
vector<string> v;
// ...
return v; // v是左值,但优先移动,不支持移动时仍可复制。
}
如果函数按值返回,ret
urn语句又直接返回了一个栈上的左值对象(输入参数除外)时,标准要求优先调用移动构造函数,如果不符再调用拷贝构造函数。尽管v是左值,仍然会优先采用移动语义,返回vector从此变得云淡风轻。此外,无论移动或是拷贝,可能的情况下仍然适用编译器优化,但语义不受影响。对于std::unique_ptr来说,这简直就是福音。
unique_ptr<SomeObj> create_obj(/*...*/) {
unique_ptr<SomeObj> ptr(new SomeObj(/*...*/));
ptr->foo(); // 一些可能的初始化
return ptr;
}
当然还有更简单的形式unique_ptr<SomeObj> create_obj(/*...*/) {
return unique_ptr<SomeObj>(new SomeObj(/*...*/));
}
在工厂类中,这样的语义是非常常见的。返回unique_ptr能够明确对所构造对象的所有权转移,特别的,这样的工厂类返回值可以被忽略而不会造成内存泄露。上面两种形式分别返回栈上的左值和右值,但都适用移动语义(unique_ptr不支持拷贝)。接收右值表达式没有移动语义时,以表达式的值(例为函数调用)初始化对象或者给对象赋值是这样的:
vector<string> str_split(const string& s);
vector<string> v = str_split("1,2,3"); // 返回的vector用以拷贝构造对象v。为v申请堆内存,复制数据,然后析构临时对象(释放堆内存)。
vector<string> v2;
v2 = str_split("1,2,3"); // 返回的vector被复制给对象v(拷贝赋值操作符)。需要先清理v2中原有数据,将临时对象中的数据复制给v2,然后析构临时对象。
注:v的拷贝构造调用有可能被优化掉,尽管如此在语义上仍然是有一次拷贝操作。同样的代码,在支持移动语义的世界里就变得更美好了。vector<string> str_split(const string& s);
vector<string> v = str_split("1,2,3"); // 返回的vector用以移动构造对象v。v直接取走临时对象的堆上内存,无需新申请。之后临时对象成为空壳,不再拥有任何资源,析构时也无需释放堆内存。
vector<string> v2;
v2 = str_split("1,2,3"); // 返回的vector被移动给对象v(移动赋值操作符)。先释放v2原有数据,然后直接从返回值中取走数据,然后返回值被析构。
注:v的移动构造调用有可能被优化掉,尽管如此在语义上仍然是有一次移动操作。不用多说也知道上面的形式是多么常用和自然。而且这里完全没有任何对右值引用的显式使用,性能提升却默默的实现了。对象存入容器这个问题和前面的构造函数传参是类似的。不同的是这里是按两种引用分别传参。参见std::vector的push_back函数。void push_back( const T& value );
// (1)
void push_back( T&& value );
// (2)不用多说自然是左值调用1右值调用2。如果你要往容器内放入超大对象,那么版本2自然是不2选择。
vector<vector<string>> vv;
vector<string> v = {"123", "456"};
v.push_back("789"); // 临时构造的string类型右值被移动进容器v
vv.push_back(move(v)); // 显式将v移动进vv
困扰多年的难言之隐是不是一洗了之了?std::vector的增长又一个隐蔽的优化。当vector的存储容量需要增长时,通常会重新申请一块内存,并把原来的内容一个个复制过去并删除。对,复制并删除,改用移动就够了。对于像vector这样的容器,如果频繁插入造成存储容量不可避免的增长时,移动语义可以带来悄无声息而且美好的优化。std::unique_ptr放入容器曾经,由于vector增长时会复制对象,像std::unique_ptr这样不可复制的对象是无法放入容器的。但实际上vector并不复制对象,而只是“移动”对象。所以随着移动语义的引入,std::unique_ptr放入std::vector成为理所当然的事情。容器中存储std::unique_ptr有太多好处。想必每个人都写过这样的代码:
MyObj::MyObj() {
for (...) {
vec.push_back(new T());
}
// ...
}
MyObj::~MyObj() {
for (vector<T*>::iterator iter = vec.begin(); iter != vec.end(); ++iter) {
if (*iter) delete *iter;
}
// ...
}
繁琐暂且不说,异常安全也是大问题。使用vector
总结
移动语义绝不是语法糖,而是带来了C++的深刻革新。移动语义不仅仅是针对库作者的,任何一个程序员都有必要去了解它。尽管你可能不会去主动为自己的类实现移动语义,但却时时刻刻都在享受移动语义带来的受益。因此这绝不意味着这是一个可有可无的东西。
作者:Tinro
链接:https://www.zhihu.com/question/22111546/answer/30801982
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
再来看看完美转发
完美转发
假设有一个函数foo,我们写出如下函数,把接受到的参数转发给foo:
template<class T>
void fwd(TYPE t)
{
foo(t);
}
我们一个个来分析:
如果TYPE是T的话,假设foo的参数引用类型,我会修改传进来的参数,那么fwd(t)和foo(t)将导致不一样的效果。
如果TYPE是T&的话,那么fwd传一个右值进来,没法接受,编译出错。
如果TYPE是T&,而且重载个const T&来接受右值,看似可以,但如果多个参数呢,你得来个排列组合的重载,因此是不通用的做法。
你很难找到一个好方法来实现它,右值引用的引入解决了这个问题,在这种上下文时,它成为forwarding reference。 这就涉及到两条原则。第一条原则是引用折叠原则:
A& & 折叠成 A&
A& && 折叠成 A&
A&& & 折叠成 A&
A&& && 折叠成 A&&
第二条是特殊模板参数推导原则:
1.如果fwd传进的是个A类型的左值,那么T被决议为A&。 2.如果fwd传进的是个A类型的右值,那么T被决议为A。
将两条原则结合起来,就可以实现完美转发。
A x;
fwd(x); //推导出fwd(A& &&) 折叠后fwd(A&)
A foo();
fwd(foo());//推导出fwd(A&& &&) 折叠后 fwd(A&&)
std::forward应用于forwarding reference,代码看起来如下:
template<class T>
void fwd(T&& t)
{
foo(std::forward<T>(t));
}
要想展开完美转发的过程,我们必须写出forward的实现。接下来就尝试forward该如何实现,分析一下,std::forward是条件cast的,T的推导类型取决于传参给t的是左值还是右值。因此,forward需要做的事情就是当且仅当右值传给t时,也就是当T推导为非引用类型时,forward需要将t(左值)转成右值。forward可以如下实现:
template<class T>
T&& forward(typename remove_reference<T>::type& t)
{
return static_cast<T&&>(t);
}
现在来看看完美转发是怎么工作的,我们预期当传进fwd的参数是左值,从forward返回的是左值引用;传进的是右值,forward返回的是右值引用。假设传给fwd是A类型的左值,那么T被推导为A&:
void fwd(A& && t)
{
foo(std::forward<A&>(t));
}
forward<A&>实例化:
A& && forward(typename remove_reference<A&>::type& t)
{
return static_cast<A& &&>(t);
}
引用折叠后:
A& forward(A& t)
{
return static_cast<A&>(t);
}
可见,符合预期。再看看传入fwd是右值时,那么T被推导为A:
void fwd(A && t)
{
foo(std::forward<A>(t));
}
forward<A>
实例化如下:
A&& forward(typename remove_reference<A>::type& t)
{
return static_cast<A&&>(t);
}
也就是:
A&& forward(A& t)
{
return static_cast<A&&>(t);
}
forward返回右值引用,很好,完全符合预期。
(摘自:http://blog.csdn.net/booirror/article/details/45057689)
完美转发(perfect forwarding)
问题是指函数模板在向其他函数传递参数时该如何保留该参数的左右值属性的问题。
也就是说函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;同样如果相应实参是右值,它就应该被转发为右值。
这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)的可能性。
如果将自身参数不分左右值一律转发为左值,其他函数就只能将转发而来的参数视为左值,从而失去针对该参数的左右值属性进行不同处理的可能性。
一个完美转发的例子
#include "stdafx.h"
#include <iostream>
using namespace std;
void fun(int &x) { cout << "lvalue ref" << endl; }
void fun(int &&x) { cout << "rvalue ref" << endl; }
void fun(const int &x) { cout << "const lvalue ref" << endl; }
void fun(const int &&x) { cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T &&t) { fun(std::forward<T>(t)); }
int _tmain(int argc, _TCHAR* argv[])
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
system("pause");
return 0;
}
可以看到,左右值属性完美地保留了。其核心就在std::forward这个模板函数。
(来源:http://blog.csdn.net/aqtata/article/details/35372769)
关于完美转发,我觉得在这之前应该先了解一下模板类型的推导机制,可以看下这篇文章:http://www.cnblogs.com/wangxiaobao/p/5829358.html
https://zhuanlan.zhihu.com/p/23884448