大家都开始C++0x了,我也来凑热闹,今天的主题是《调侃rvalue-reference》
和所有的故事一样,先来一个起因。
话说从C到C++,都有左值和右值的概念,来满足语义的需要。这与变量/对象无关,是用来解释一个表达式的类型。
- C/C++ code
int foo();int *p = &foo(); //#1p = &1; //#2
明显地,右值不能取地址。在C++中,只有const-ref才能绑定右值。例如
- C/C++ code
int &a = 0; //错误const int &b = 0; //正确
这样看起来,右值是无法被修改的,但事实上,右值是允许被修改的,但是因为绑定到const-ref则失去修改的能力。
- C/C++ code
class T{public: T():i(0){} T& set() { i = 5; return *this; } int value() const { return i; }private: int i;};int x = T().set().value();x得到5,在这个临时对象结束之前,我们修改了它的值,并正确得到了这个值,然后分号之后,这个临时对象被销毁。既然临时对象能被修改为什么不能用non-const-ref绑定呢?原因很简单。
- C/C++ code
int & r = int();r = 5; //r引用的临时对象已经失效了,分号之后就已经销毁了。const int &cr = int();int x = cr;
此时cr引用的临时对象仍然存在,该临时对象的生命期已经延长到和cr相同。cr什么时候结束,这个临时对象就在什么时候被销毁。但有时候只能用 const-ref绑定临时对象实在是很痛苦的。例如std::auto_ptr就是一个典型的例子。两个auto_ptr对象赋值,实参对象会把资源转移到目标对象。
- C/C++ code
std::auto_ptr<int> a(new int);std::auto_ptr<int> b = a;
之后,b将引用动态分配的int对象,a则断开拥有权。也就是说拷贝构造函数会修改参数对象。因此,auto_ptr的拷贝构造函数和赋值操作符的参数类型都是使用的auto_ptr&而不是const auto_ptr&。而对于
- C/C++ code
std::auto_ptr<int> foo(){ return std::auto_ptr<int>(new int);}std::auto_ptr<int> p = foo();拷贝构造函数只用auto_ptr&是不行的,因为不能绑定foo()产生的临时对象,如果用const auto_ptr&则无法修改这个参数,因为auto_ptr在赋值之后必须释放以前的拥有权。这里有两种方案,一种是用mutable成员。
- C/C++ code
template<typename T>class auto_ptr{public: auto_ptr(const auto_ptr& other) throw() :ptr(other.safe_release()) {} auto_ptr& operator=(const auto_ptr& other) throw();private: T* safe_release() const throw() { T * ret = ptr; ptr = 0; return ret; }private: T * mutable ptr;};这是不被接受的方案,auto_ptr的状态和ptr这个成员紧密相连,而auto_ptr也应该在非const的情况下状态才会改变,因此这不被接受。第二种方案,也就是标准的做法。
- C/C++ code
template<typename T>class auto_ptr{public: auto_ptr(auto_ptr& other) throw(); auto_ptr(auto_ptr_ref ref) throw(); auto_ptr& operator=(auto_ptr& other) throw(); auto_ptr& operator=(auto_ptr_ref ref) throw(); ...};用一个auto_ptr_ref来处理参数对象是右值的情况。这相当于弥补了一个语言缺陷。
对于这样的缺陷,C++加入一种新的引用类型来弥补这个问题,现在要说的就是右值引用。
右值引用主要用来绑定右值。
- C/C++ code
int foo();const int cfoo();int &&r = foo();const int &&cr = cfoo();//同样,也能绑定左值。int i;int &&r = i;const int ci;const int&& cr = ci;
右值引用的引入使C++变得更加复杂,难以学习,但是使用右值引用会让代码变得更简单,有时甚至是难以想象。对于隐晦论者,不知道怎么看待这样的问题。
首先,右值引用有一点很特殊。具名的右值引用被当作左值,无名的右值引用则仍然是右值。
例如,
- C/C++ code
int &&r = 0; //r被当作左值看待。int&& foo();foo(); //foo的返回类型是右值引用,其仍然是右值。
正是因为这个特性,使右值引用变得很复杂。但是其优点将在后面Perfect Forwarding部分介绍。
右值引用的引入确立了两东西,Move Semantics和Perfect Forwarding。英文上对于两词的表达对于我们来说尚为抽象,在适当时候我会用中文来表达。
1,Move Semantics(转移语义)
转移语义不同于拷贝语义,例如,两个auto_ptr对象的赋值操作,其实就是转移资源,而不是拷贝资源。用代码表达就是
- C/C++ code
class T{public: T():p(new int){} T(T& t) :p(t.p) { t.p = 0; } T& operator=(T& t) { if(this != &t) { delete p; p = t.p; t.p = 0; } return *this; }private: int *p;};T a;T b(a);T c;c = b;构造a的时候会动态分配一个int对象,然后a引用这个对象。构造b的时候,调用拷贝构造函数,这时a会将那个动态分配的int对象传递给b,则自己不再引用。然后c=b的赋值,b同样会把这个int对象转移给c,而自己则不在引用。这样,这个int对象,就从a转移到了b,再转移到c,而没有拷贝这个 int对象,这就是所谓的转移语义,auto_ptr也是如此。转移语义到底有什么作用?考虑一下这个情况。
- C/C++ code
std::vector<std::string> v;
v 里面保存了很多std::string对象,push_back操作会将buffer用完,然后重新分配更大的buffer,并将老buffer上的所有 std::string对象拷贝赋值到新buffer中,这个过程是很耗时的,因为每一个新的对象会被拷贝构造,然后分配内存,将老string对象的字符buffer复制到新的string对象里,然后老的被销毁,并释放字符buffer。如果std::string支持转移语义则情况大为改观,构造时,老的string对象只需要把字符buffer转移到新的string对象即可,没有了内存分配和释放的动作,性能也会大大提高。
有人纳闷了,如果std::string也支持转移语义,那就跟auto_ptr一样了,不能用在标准的STL容器里了。其实不然,因为现在C++不支持右值引用,它的拷贝构造函数并不是auto_ptr(const auto_ptr&),而STL容器则需要有拷贝语义,也就是需要元素有T(const T&)这样的拷贝构造函数。而如果让std::string支持转移语义并不会与现存的拷贝语义发生冲突。例如,加入转移语义的 std::string看起来就像是下面这样
- C/C++ code
template < class CharType, class Traits=char_traits<CharType>, class Allocator=allocator<CharType>>class basic_string{public: basic_string(const basic_string& _Right, size_type _Roff = 0, size_type _Count = npos, const allocator_type& _Al = Allocator ( ) ); //拷贝构造函数 basic_string(basic_string&& _Right, size_type _Roff = 0, size_type _Count = npos, const allocator_type& _Al = Allocator ( ) ); //转移构造函数 basic_string& operator=(const basic_string&); //拷贝赋值操作符 basic_string& operator=(basic_string&&); //转移赋值操作符};其中basic_string&&就是右值引用, 可以用来绑定右值。例如
- C/C++ code
std::string foo(){ return "Hello, World";}std::string str;str = foo(); //没有了字符串的拷贝动作。细心的人在这里也许会发现一个缺陷。假如,我们定义一个具有转移语义的类,并在这个类里面使用具有转移语义的std::string。
- C/C++ code
class T{public: T(const T& other) //拷贝构造 :text(other.text) {} T(T&& other) //转移构造 :text(other.text) {} T& operator=(const T& other) //拷贝赋值操作符 { if(this != &other) { text = other.text; } return *this; } T& operator=(T&& other) //转移赋值操作符 { if(this != &other) { text = other.text; } return *this; }private: std::string text;};在前面介绍的右值引用的一个特性,发现有什么问题了吗?这里的text成员不会调用转移构造函数和转移赋值操作符。因为在T的转移构造函数和转移赋值操作符中,参数other是有名字的右值引用,因此它被当作了左值
- C/C++ code
T(T&& other) //转移构造 :text(other.text) //调用拷贝构造函数 {} T& operator=(T&& other) //转移赋值操作符 { if(this != &other) { text = other.text; //调用拷贝赋值操作符 } return *this; }
也许有人立马会站出来说这是极大的隐晦。其实不然,如果知道了右值引用的特性和重载解析就不会发生这样的错误。解决这个问题的办法就是让传递给text的参数变成右值。往回看,在讲右值引用之初已经提到了一点。标准库也提供了一个move函数用来做转换。
- C/C++ code
namespace std{ template <typename T> typename remove_reference<T>::type&& move(T&& a) { return a; }}remove_reference<T>::type就是得到一个解引用的类型。然后T的转移构造函数和转移赋值操作符就写成
- C/C++ code
T(T&& other) //转移构造 :text(std::move(other.text)) {} T& operator=(T&& other) //转移赋值操作符 { if(this != &other) { text = std::move(other.text); } return *this; }通过std::move一个间接调用,使实名的右值引用转换成无名的,这样就被当作右值处理。
2,Perfect Forwarding(精确转递)
有些时候我们会设计出一种管理器,用来保存所有的对象。例如窗口类
- C/C++ code
class window{//...};然后这个管理器会有一个接口用来创建指定类型对象。
- C/C++ code
template<typename Window>window* factory(){ return (new Window);}window* w = factory<Window>();其实这样的factory是远远不够的。因为我们有时会从class window派生。例如
- C/C++ code
class msg_window :public window{public: msg_window(const std::string& text);};class input_window : public window{public: input_window(std::string& text);};factory就会写成template<typename Window, typename T>window* factory(const T&);和template<typename Window, typename T>window* factory(T&);如果派生类的构造函数有两个参数,那factory就要重载4个版本。这可不是容易的活。现在用右值引用可以方便地解决这个问题。对于一个参数的版本
- C/C++ code
namespace std{ template<typename T> struct identity { typedef T type; }; template<typename T> T&& forward(typename identity<T>::type&& t) { return t; }}template<typename Window, typename T>window* factory(T&& t){ return (new Window(std::forward<T>(t)));}window *msg = factory<msg_window>(std::string("Hello"));std::string text;window *input = factory<input_window>(text);一个factory版本就能处理两种类型的参数。是不是很方便?这完全是依靠右值引用。在这里会涉及函数模板参数的推导,对于右值引用来说,这里有一个很重要的过程。例如下面的代码
- C/C++ code
template<typename T>void f(T&& t);int i;f(i); //#1f(2); //#2
#1推导结果就是f<int&>(i),这时f的参数t类型就是int&,这就是那个重要的地方。如果模板参数T是左值引用,那T&&的类型也是左值引用,例如#1推导出来的T是 int&,然后f的参数T&&也会被转换成int&。
#2推导结果就是f<int>(i),这时f的参数t类型就是int&&
在这里std::forward看上去跟std::move差不多,但为什么需要一个identity呢?这是为了防止模板参数的推导。现在我们不考虑identity的情况。
- C/C++ code
template<typename T>T&& forward(T&& t){ return t;}和std::move完全一样,将实名的右值引用转换为无名右值引用。在调用forward的时候,模板参数T则被编译器自动推导,这就出现问题了,例如上面的factory,我们改成forward自动推导的版本。
- C/C++ code
template<typename Window, typename T>window* factory(T&& t){ return (new Window(std::forward(t)));}
t是实名的右值引用,因此被看作是左值,则std::forward返回的也是左值。对于这种情况,下面代码就能体现出一个错误
- C/C++ code
class test_window : public window{public: test_window(const std::string&); test_window(std::string&);};factory<test_window>(std::string("Hello"));会调用test_window(std::string&)来构造test_window对象,因为没有identity版本的forward将参数推导为左值,返回的也成了左值,这并不是我们期望的,std::string("Hello")创建的是临时对象。
那 identity在这里有什么用呢?为什么能解决这个问题呢?其实它只是起到一个显式指定模板参数的作用。在有identity的版本。直接写 std::forward(t)将会抱错,无法推导模板参数,而必须显式指定,从而避免了模板参数的推导。这里借用上面的test_window来解释这一点。
- C/C++ code
template<typename Window, typename T>window* factory(T&& t){ return (new Window(std::forward<T>(t)));}std::string text;factory<test_window>(text); //#1factory<test_window>(std::string("Hello")); //#2#1,text 是左值,则factory的模板参数T为std::string&,而T&&也就是std::string&,那么参数 t的类型也是std::string&,然后std::forward<std::string&>(t)也就返回的是 std::string&,仍然是左值,那么#1就会调用test_window(std::string&)来构造 test_window,符合本意。
#2,std::string("Hello")创建了一个临时对象,则factor的模板参数T为 std::string,而T&&则为std::string&&,然后 forward<std::string&&>(t)最后返回的仍然是右值。因此调用test_window(const std::string&)构造。同样符合本意。
这里std::forward<>就执行了一个Perfect forwarding,也就是精确转递,将参数t的类型精确地转递到Window的构造上。因此factory为每种参数个数的版本,只需实现一个,而不是(N^2 - 1)个。这在写开放代码,例如库的实现尤为重要。
-完-
觉得我的故事讲的不清晰的请举右手。觉得我将的清晰的请举脑袋。
http://blog.csdn.net/Jinhao/archive/2009/05/08/4159299.aspx
[解决办法]
sf
[解决办法]
帮顶+接分吧!
吼吼吼~~~
[解决办法]
mark
[解决办法]
mark
[解决办法]
来学习了
[解决办法]
呃,看晕了。
[解决办法]
我先做个标记,然后再看?
[解决办法]
收藏,神木王“顶”
[解决办法]
学习
[解决办法]
就是有点长啊,我怎么没耐心看?
[解决办法]
你怎么这么能写啊?
[解决办法]
哥哥,老实说,我没看懂你写的了,主要是排版太乱,前言不搭后语的。。。
[解决办法]
很长很强大
除了膜拜都不敢看一眼
[解决办法]
举右手,我觉得主要是叙述和代码直接的承接问题,说得太“洗练”了
顺便问下,300分,我能得到多少。
我之前按照我的方式,理解过rvalue-ref的,我记得人家都叫第二点为 完美转发的,~
[解决办法]
进来学习下~
[解决办法]
mark
不太懂什么意思
[解决办法]
mark..........
拜读下。。。
------解决方案--------------------
[解决办法]
+_+
[解决办法]
学习下
[解决办法]
这个要顶
[解决办法]
看了快二十分钟,终于重头到尾拜读了一遍,感觉需要举右手N次。
晚上回去继续看,不懂再举手。
[解决办法]
见刘未鹏的blog...
见维基c++0x
[解决办法]
回帖之后才发现原来盖了这么多楼了,当初我看了两分钟,然后
刷屏时sf还在呢。。
没遍历过一遍不敢随便回帖啊。
[解决办法]
UP一下
[解决办法]
先占座,有时间回来学习。
[解决办法]
谢谢!
等我锁几个帖子,再来详细拜读!
[解决办法]
占座。
[解决办法]
等分等急了!!!!!!!!!
[解决办法]
留名关注,拜读