关于右值引用的粗略研究

来源:互联网 发布:c语言 fprintf 编辑:程序博客网 时间:2024/05/16 18:45

   最好的文档莫过于标准化委员会公布的那几个最终草案: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2118.html

 这里只是想从实践者的立场出发来看这些东西。


左值和右值:当一个非引用类型对象的名称(注意这里是“名称”而不是“值”)对于处于某个作用域的观察者而言处于可见(这里的“可见”与访问权限无关)状态时,这个对象在该作用域中就是左值,反之则是右值。(注意左右值的概念在C++03开始就和老版本的标准有区别了)

例子:

int i1 = 0;

这里,i1是左值,0是右值。

int i2 = i1 * 2;

i2是左值,2是右值,i1 * 2的结果也是右值,而i1是左值。注意在现行的C++标准里,等号右边的值不再一定是右值了;凡是有名字的都是左值(但是有一个特殊情况*)。

long i3 = i2;

i3、i2都是左值,而隐式(或显示)转换存在一个(long)i2的结果,它是右值。再注意一点:值指的是表达式的结果:(long)i2和i1 * 2的结果是右值,而(long)i2和i1 * 2自身只是表达式而已。

long i4 = i3;

i4、i3都是左值,这里不存在右值。

 设A是一个类:

A o1;

o1是左值,他的成员也是

A o2 = o1 + A();

这里左值有:o1、o2及他们的成员,右值有:'+'操作符的第2个参数、o1+A()的返回值。


** 1.有个特殊情况需要考虑。见下面的例子:

[cpp] view plain copy
print?
  1. #include <conio.h>  
  2. #include <stdio.h>  
  3. #include <iostream>  
  4.   
  5.   
  6. struct A // Compiled but...  
  7. {  
  8.     A(void) {};  
  9.     A(const A &) = delete;  
  10.     A(A &&) {};  
  11. };  
  12.   
  13. void foo(A &) {}; // #1  
  14. void foo(A &&) {};// #2  
  15.   
  16. A func(void)  
  17. {  
  18.     A temp;  
  19.   
  20.     foo(temp); // Only the #1 will be allowed to call.  
  21.   
  22.     return temp;// Although the 'temp' is a named object, it will still be treated like an rvalue at this line.  
  23.     //return std::move(temp); // OK.  
  24. }  
  25.   
  26. int main(void)  
  27. {  
  28.     func();  
  29.   
  30.     _getch();  
  31.     return(0);  
  32. }  

这个例子里因为func内部的局部变量对于func的调用者来说(注意后面的几点将要提到“而根据观察者所处位置的不同,同一个对象的左右值属性也不一定相同”)亦可看作是临时对象而使得左值在可能做削减的特定位置(比如这里的return语句处)被当作右值处理。当然可能仍需要提醒的是,仅仅是在特定位置当作右值,而其他位置仍然以左值看待(比如如果在return语句之前某个位置使用那里的temp对象,仍然只能在那个位置被当作左值而不会是右值)。标准中相关的规定则是

12.8 [class.copy]:

32 - 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. [...]


引用:概念略过...需要说的是:

-右值引用 写作 &&的形式,还有就是 右值引用 不可以绑定到 左值。非 常左值引用 原则上也不宜绑定到右值

-无名对象如果原本是个左值引用,那这个对象还是左值。

 

下面的叙述顺序并不说明这些是有某种顺序的~~

-右值不可能取到地址 试图对一个右值写&来取指针只会导致语法错误

-引用一定是个左值 其实很简单的就是根据左右值的概念去理解,右值引用的定义只是说明被引用的对象是一个右值,并没说引用本身的左右值属性。而根据具名就是左值的观点,右值引用名本身其实是一个左值。

-右值属性只是个绑定协议 如果说某个值是右值,其实是说对这个值的直接使用者而言其名称不可见(例如临时对象),而根据观察者所处位置的不同,同一个对象的左右值属性也不一定相同。但是对于某个确定位置的观察者来说,左右值属性是可分析的、不变的。例如,就像那些文章里的那样:

void foo(int && i)

{}

在这个函数的外部,传给函数的参数是右值,就是对于函数的调用者而言的,而函数的内部,i是左值,即使是i引用的实际对象也是。对i的取地址也是合法的。

-一个对象对于自身而言永远是左值 别忘了前面还说过“右值不可能取到地址”。如果这个“右值”是一个类对象,对象的内部就有个'this'指针。

-生存期问题

在同一个作用域内部:对于一个左值来说,他的引用在这个左值所在函数内部总是后创建的(因为引用声明时必须先初始化),因此肯定先于这个左值被“销毁”至不可见,所以肯定是访问安全的(只要不转换成指针);而对于右值来说,其右值引用其实是将右值当左值使用,因此也不存在什么问题。

    不过,实践证明,右值与左值之间不完全遵循生命期的构造与析构的后进先出顺序。例如

A o1 = A() + A();

这个式子里,最先构造的是第一个和第二个加法操作的参数(临时对象),然后构造的是加法操作的返回值(也是临时对象),最后才是o1。但是接下来马上析构掉3个临时对象,而o1要到他所在的作用域末端才开始析构。但是这个是怎么实现的呢?

    其实编译器做了一些处理(这可能也是C/C++为什么决定要将函数返回类型放到声明最开始的原因之一吧):最先入栈的是返回值的内存单元,但是返回值的构造函数却没有被调用;直到return执行期间才开始调用构造函数。因此不会出现调用栈“断裂”的问题。

 

跨 函数/结构体/类 作用域:当一个引用被当作函数返回值、类或结构体的成员时,情况发生了变化。引用指向的对象完全有先于引用退栈的可能,结果引用的对象就会失效。不管是左值还是右值都存在这种危险。(以下为错误的代码段)

[cpp] view plain copy
print?
  1. A & foobad(void)  
  2. {// buggy  
  3.     A o;  
  4.   
  5.     return(o);  
  6. }  
  7.   
  8. ...  
  9.   
  10. A & o1 = foobad();//error  

 以上代码段解读:有人可能会说,既然这样不安全,为什么标准干脆规定不得返回引用得了...如果是这样,一来这个机制还是可能被绕过从而失效;二来,一般返回引用更常用的是被大量的类用来对他们的客户暴露他们维护的内部对象的一种方法。所以出现上面的代码只能说是误解了允许函数返回引用的真实目的。

 

[cpp] view plain copy
print?
  1. struct TAG  
  2. {  
  3.     A && q;  
  4. };  
  5.   
  6. ...  
  7.   
  8. TAG tst = { A() };//buggy  
  9.   
  10. ... = tst.q;//error  

 

-关于“完美转发”的问题 有关的内容从下面诺干点开始说吧(注意C++0x的实现和C++11有所不同。C++0x仅是测试版,应该以C++11为准)

-'::'操作符并不能阻挡模板类型推导

    例如下面的代码:

[cpp] view plain copy
print?
  1. template <typename T>  
  2. void foo(T && obj)  
  3. {  
  4.     return;  
  5. }  
  6.   
  7. ...  
  8.   
  9. A o1;  
  10.   
  11. foo(o1);  
  12. foo(A());  


省略号之下的内容请放进main函数或能直接执行到的函数内。然后你可以在foo的return处下一个断点然后运行。分别在执行第一个和第二个foo期间断点中断时查看一下obj的实际类型。如果观察到第一次为A&而第二次为A&&时就可以了。

接下来我们将foo改成下面的样子:

[cpp] view plain copy
print?
  1. template <typename T>  
  2. struct TAG  
  3. {  
  4.     typedef T typeId;  
  5. };  
  6.   
  7. template <typename T>  
  8. void foo(typename TAG<T &&>::typeId obj)  
  9. {  
  10.     return;  
  11. }  


然后编译。你会发现编译失败!现在这样的写法,编译器已经无法自动推导出TAG和typeId的类型了。

接下来你改动两个调用处为手动匹配:

foo<A &&>(o1);

foo<A &&>(A());

编译,你会发现第一个编译不成功。到目前为止,似乎::隔离还是有效的。但是别忘了这是手动指定的类型让编译器去识别。

现在把刚才这两个foo的调用改回去!

然后把之前的foo定义全部删除,重新贴上下面的内容:

[cpp] view plain copy
print?
  1. template <typename T>  
  2. struct TAG  
  3. {  
  4.     typedef T typeId;  
  5. };  
  6.   
  7. template <typename T>  
  8. void foo2(typename TAG<T &&>::typeId obj)  
  9. {  
  10.     return;  
  11. }  
  12.   
  13. template <typename T>  
  14. void foo(T && obj)  
  15. {  
  16.     foo2<T>((T &&)obj);  
  17.     return;  
  18. }  

在foo2的return处下一个断点,然后运行、断下,查看其obj的类型,结果发现:

1。编译并没有通不过,看来编译器可能没有将第一个调用也当作B&&处理。

2。运行时发现第一次断下时obj的类型为B&,第二次为B&&,证明编译时确实将模板类型T的真实类型穿透给了TAG的TypeId。说明之前对1处的猜测是正确的。

总结:在C++11中,::操作符并没有阻止模板类型的推导(阻止的只是模板函数参数的自动匹配,即不会让你能从::操作符自动推导出::之前的类的类型。)


-最近修订的“完美转发”靠的是模板特化和去引用机制了

    其实这里只需要明确一下模板实例化或特化的规则即可:

1. 当类型是通过参数自动匹配到的时候(比如函数模版,在没有显式指名模板类型参数的情况下匹配到的模板类型、左右值及常属性),以直接类型为最高优先级匹配-〉模板类型的函数参数只有在带右值引用符号的情况下才保留左右值特征。如果可以携带右值特征,则根据上面的第1点决定是否要再构造个临时对象取代原传入参数。

2. 如果模板参数在实例化时是被显式传入的,则在这个实例的作用域内将该模板参数用作其它模板参数或用来作为变量类型声明时,左右值信息、常属性信息都不会丢失。

3. 对于内嵌的typedef,输出类型仍然尽可能保留全部信息(C++11和之前的C++0x不同的地方,之前的版本中,类型信息会发生退化),但是当从模板外部用作函数参数类型时,该函数将失去对这个参数进行类型自动推导的能力(只是失去自动推导的能力,即需要显示指名类型参数而已;而不是失去左右值及常属性信息)。

 

-移动仅是一个匹配协议:

1. void foo(T && );和void foo(const T & );、void foo(T &);使用中到底有什么不同?

对于函数体本身,传入参数其实完全没有区别(和第二个函数也只有常属性的区别)。如果函数实现完全一致,编译后的函数体也会完全一样。编译器不会为右值引用为函数体放置任何额外的代码

对于函数的调用者来说,完全是传入参数左右属性和常属性的区别(仅仅是形式上的)。

2. 移动操作并不会销毁源对象

移动操作只是提供了一个“源对象的内容可以被直接拿到目标对象“的通知。具体的实现则完全取决于作者。实际上移动完成后,源对象仍然应该是个有效的对象,仅仅是内容发生改变而已。析构的位置仍然由其生命期作用域决定。移动不应被当作源对象的析构函数使用,或者说仅仅是让后续的析构函数不会析构掉目标对象已经挪走的内容


- 右值引用并不一定能提高拷贝性能(指望写个右值引用重载函数就能加速的童鞋们可要失望了)

右值引用能提高性能,其实是通过去除重复深度构造进行的,让接受对象认识到源的内容”反正已经不要了,直接拿来即可”。而通过什么途径“拿来”才是关键问题。对于值成员来说,显然再快也需要通过拷贝的途径。而如果是指针成员指向的内容,则拷贝那个指针就等于拿到了指向的内容,但是指针本身仍然需要拷贝。如果一个类仅有值成员而不包含指针,显然移动的代价至少也会和拷贝时的完全相同。所以说右值引用只是帮你完成了省去重新构建和拷贝指针指向的内容的工作,并没有其余的优化手段。


- 右值引用应慎用于多对象操作的场合,比如容器

一开始可能会感觉比较奇怪的是,既然C++11已经支持了右值引用,为什么对应的标准库中容器的算法仍然有重分配和拷贝动作,为什么不直接改用移动得了*?原因是异常安全。比如现在某个容器C需要重整,C内已有有元素a1和a2。当C分配了新的存储空间后,如果使用移动算法,首先成功地移动了a1,但是接下来a2移动的过程中产生了异常。按照容器一般的设计,这种情况是需要回滚的。但是怎样能可靠的回滚?如果移动a1到原来的位置又产生异常,这样容器将进入无法恢复的状态。

而拷贝则不同。如果出现类似情况,大不了析构掉新位置的元素,然后释放掉新位置所需的存储空间*,而原位置的元素继续保持有效。这样即可成功的恢复到操作前的状态(就当什么也没发生)。而想利用到标准库对移动操作的支持,则需要做一些调整使你的代码能够向容器提供你的元素类能够执行无异常的移动的信息(这方面可以搜索你用的编译器异常安全相关的资料。考虑到目前编译器的实现度不同,不做详细描述)。

另一个使用场景就是,虽然右值引用的出发点很好,但是毕竟它会修改源对象的内容。当源对象是个真正的临时对象的时候这没有什么问题。但是别忘了还有个move操作的存在(左值当右值使用)。这个操作会导致移动操作失败(比如移动一部分内容后抛出了异常)的情况下源对象也无法恢复的结果。所以请记住:如果你认为你的程序设计上不允许这样的行为,就不要在这种场景中使用右值引用(最好根本不提供相关的函数接口。因为你无法预料用户会用真正的右值还是从左值利用move模拟一个右值)。

**1.在支持noexcept关键字的编译系统上,良好推导出拷贝构造函数具有noexcept属性的类对象做容器内元素,容器会自动匹配到移动

**2.注意这里做了一个析构函数不会抛出异常的假定。而一个良好设计的程序应当禁止设计出可能抛出异常的析构函数


介绍几个左右值相关的概念。此类概念只是描述上的概念而不涉及编译器语法:

lvalue: 左值(left-value):

rvalue: 右值(right-value):

xvalue: 待失效值(expiring-value):

prvalue: 纯右值(pure-rvalue):

glvalue: 泛左值(generalized-lvalue):

原创粉丝点击