首页 > 代码库 > POJ 3171

POJ 3171

一 C++中表达式的分类

传统C++的变量表达式分为左值和右值。通俗来讲,两者有着如下区别:

1.从生命周期上来看:左值就是非临时对象,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效;

2.左值是有名字的(通过其他具名对象间接得到的,例如通过返回引用的函数,或通过指针解引用(* 运算符)),有固定的存放数据的地址。而右值则是匿名的,相同的右值表达式多次引用,其位置(在寄存器中,或内存地址)不一定相同。


原来这左值,右值两者之间界限明显,而且不能相互转换。一个左值想要得到右值的值,必须要做复制操作(Copy Semantic)。比如说string a_string=string("hello")。string("hello")是个右值,a_string要复制这个右值的值。string("hello")本身这个临时对象不并能成为左值。但xvalue的出现使得右值到左值成为可能。


xvalue(eXpering value消亡值)的是指快要销毁或被转移(move)的对象,它既有右值的一些特性,又有左值的一些特性。所以和原来的右值prvalue(pure value纯右值)一起被归类到rvalue中,而又与左值一起被归类到glvalue中(generialized lvalue)中。xvalue实质上是一个右值,但是我们却可以将其当做一个左值来用。


如何是一个右值(prvalue)成为一个xvalue呢?通过右值引用。左值引用的声明符号为”&”, 为了和左值区分,右值引用的声明符号为”&&”。被右值引用的表达式(必须是一个右值)可以通过函数参数传递和函数返回变为xvalue——一个函数返回右值引用,则其返回的是一个xvalue。一个函数的参数申明为右值引用类型,则传参时,参数又从ralue变为xvalue。

二.右值引用
示例程序 :
 void process_value(int& i) { 
  std::cout << "LValue processed: " << i << std::endl; 
 } 


 void process_value(int&& i) { 
  std::cout << "RValue processed: " << i << std::endl; 
 } 


 int main() { 
  int a = 0; 
  process_value(a); 
  process_value(1); 
 }
运行结果 :
 LValue processed: 0 
 RValue processed: 1
Process_value 函数被重载,分别接受左值和右值。由输出结果可以看出,临时对象是作为右值处理的。
但是如果临时对象通过一个接受右值的函数传递给另一个函数时,就会变成左值,因为这个临时对象在传递过程中,变成了命名对象。(xvalue——>lvalue)。这个过程的c++内部实现中用到了Move Semantic。关于Move Semantic及其对于我们编程者有何作用,后文将详细提到。
示例程序 :
 void process_value(int& i) { 
  std::cout << "LValue processed: " << i << std::endl; 
 } 


 void process_value(int&& i) { 
  std::cout << "RValue processed: " << i << std::endl; 
 } 


 void forward_value(int&& i) { 
  process_value(i); 
 } 


 int main() { 
  int a = 0; 
  process_value(a); 
  process_value(1); 
  forward_value(2); 
 }

运行结果 :
 LValue processed: 0 
 RValue processed: 1 
 LValue processed: 2
虽然 2 这个立即数在函数 forward_value 接收时是右值,但到了 process_value 接收时,变成了左值。多次使用一个临时变量(即右值引用)到底能带来怎样的好处呢?右值引用的好处主要体现在move semantic(转移语义)中。

三. Move Semantic
转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。
举例:
 int main() { 
  MyString a; 
  a = MyString("Hello"); 
  std::vector<MyString> vec; 
  vec.push_back(MyString("World")); 
 }
MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。
我们可以通过比较复制构造函数和转移构造函数的差别来体现Move Semantic的优势。
复制构造函数:
MyString(const MyString& str) { 
    _len = str._len; 
    _data = http://www.mamicode.com/new char[_len+1]; >

转移构造函数:
 MyString(MyString&& str) { 
    std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
    _len = str._len; 
    _data = http://www.mamicode.com/str._data; >
普通的的复制构造函数要new一个新的空间来构造对象。而转移构造函数将临时对象xvalue变成了一个有名变量(lvalue)。不需要销毁临时对象,我们也不需要对新对象进行申请空间,创建对象等等工作了。所以vec.push_back(MyString("World"));这条语句就不会有不必要的临时对象的创建、拷贝以及销毁的浪费了。而还可以通过定义转移赋值函符来减少赋值是的不必要的临时对象的创建、拷贝以及销毁的浪费。
  MyString& operator=(MyString&& str) { 
    std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
    if (this != &str) { 
      _len = str._len; 
      _data = http://www.mamicode.com/str._data; >

还有一种情况,如果赋值构造或是复制构造是发生在左值到左值之间,通常应该为复制语义(Copy Semantic)。比如String a_string=b_string;或是vec.push_back(b_string)。Copy之后,a_string和b_string还是两个独立可用的对象。但b_string再赋值或是复制之后就没什么用了,那我们应该还是做转移操作。我们可以将通过std::move(b_string)将b_string由一个lvalue转换成xvalue。接下来String a_string=b_string;或是vec.push_back(b_string)这两个语句就会进行转移语义。b_string将被再可用。(如果用它会发生什么情况???)
STL中的move函数就是一个典型的运用std::move的例子:
template<typename T>
void swap(T& a, T& b)
{
    T t(std::move(a));  // a为空,t占有a的初始数据
    a = std::move(b); //  b为空, a占有b的初始数据
    b = std::move(t); // t为空,b占有a的初始数据
} 

总结一下,现在lvalue和rvalue之间的界限就不是很明显了,lvalue可以通过std::move变为xvalue,rvalue碰到右值申明时,也会变成xvalue。xvalue可以通过Move Semantic变为lvalue。

四.编译器优化与Move Semantic不冲突
     未完成