读书人

大家都开始C++0x了,小弟我也来凑热闹,

发布时间: 2012-02-25 10:01:49 作者: rapoo

大家都开始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一下
[解决办法]
先占座,有时间回来学习。
[解决办法]
谢谢!
等我锁几个帖子,再来详细拜读!
[解决办法]
占座。
[解决办法]
等分等急了!!!!!!!!!
[解决办法]
留名关注,拜读

读书人网 >C++

热点推荐