本文介绍了C++11中的move语义,以及右值引用的产生逻辑。
 
性能问题 (1) 首先,看下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include  <iostream>  #include  <string.h>  class  Foo {   private :     char  * data;   public :     virtual  ~Foo()     {       std ::cout  << "~Foo()    "  << (void *)this  << "/"  << (void *)data << std ::endl ;       if (data != NULL )       {         delete  []data;         data = NULL ;       }     }     Foo(const  char  * s) : data(NULL )     {       std ::cout  << "Foo(const char * s)    " ;       if (s)       {         int  len = strlen (s);         data = new  char [len+1 ];         memcpy (data, s, len+1 );       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;     }     Foo(const  Foo & other) : data(NULL )     {       std ::cout  << "Foo(const Foo & other)    "  << (void *)&other << "/"  << (void *)other.data << "-->" ;       if (other.data)       {         int  len = strlen (other.data);         data = new  char [len+1 ];         memcpy (data, other.data, len+1 );       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;     }     Foo& operator = (const  Foo & other)     {       std ::cout  << "Foo& operator= (const Foo & other)    "  << (void *)&other << "/"  << (void *)other.data << "==>" ;       if (this  != &other)       {         if (NULL  != data)         {           delete  []data;           data = NULL ;         }         if (NULL  != other.data)         {           int  len = strlen (other.data);           data = new  char [len+1 ];           memcpy (data, other.data, len+1 );         }       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;       return  *this ;     } }; Foo func1 ()   {  Foo f1 ("abc" )  ;   return  f1; } int  main ()  {  Foo f2 = func1();   return  0 ; } 
 
编译(编译的时候,加选项-fno-elide-constructors是为了禁止RVO优化(return value optimization),这样,更容易暴露性能问题):
1 g++ -fno-elide-constructors --std=c++11 Foo1.cpp -o foo1 
 
运行 foo1  输出如下:
1 2 3 4 5 6 Foo(const char * s)    0x7fff8af40050/0x21e8010 Foo(const Foo & other)    0x7fff8af40050/0x21e8010-->0x7fff8af40090/0x21e8030 ~Foo()    0x7fff8af40050/0x21e8010 Foo(const Foo & other)    0x7fff8af40090/0x21e8030-->0x7fff8af40080/0x21e8010 ~Foo()    0x7fff8af40090/0x21e8030 ~Foo()    0x7fff8af40080/0x21e8010 
 
现在来看性能问题:
输出第1行:在函数func1中,构造Foo对象f1(调用构造函数),其data在堆空间的0x21e8010处。 
输出第2行:函数func1返回时,把f1拷贝到临时对象(调用拷贝构造函数),其data在堆空间0x21e8030处。 
输出第3行:析构f1。 
输出第4行:在main函数中,把函数func1返回的临时对象拷贝到f2(调用拷贝构造函数),其data在堆空间0x21e8010处。注意f1.data和f2.data地址相同,但这只是一个巧合而已,它们是不同的字符串:f1被析构后,构造f2分配堆空间,恰好分配到相同的地址。 
输出第5行:析构临时对象。 
输出第6行:析构f2。 
 
可以看出,为避免浅拷贝问题,Foo对象的每一次拷贝,都需要分配堆空间,若class Foo很复杂,有很多指针成员,拷贝将带来更大的性能损耗。另一方面,f1不会再被访问,而临时对象不能被访问,它们非得拥有一个完整的拷贝吗?我们来尝试优化一下。
优化尝试 (2) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include  <iostream>  #include  <string.h>  class  Foo {   private :     char  * data;   public :     virtual  ~Foo()     {       std ::cout  << "~Foo()    "  << (void *)this  << "/"  << (void *)data << std ::endl ;       if (data != NULL )       {         delete  []data;         data = NULL ;       }     }     Foo(const  char  * s) : data(NULL )     {       std ::cout  << "Foo(const char * s)    " ;       if (s)       {         int  len = strlen (s);         data = new  char [len+1 ];         memcpy (data, s, len+1 );       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;     }     Foo(Foo & other)     {       std ::cout  << "Foo(Foo & other)    "  << (void *)&other << "/"  << (void *)other.data << "-->" ;       data = other.data;       other.data = NULL ;       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;     }     Foo& operator = (Foo & other)     {       std ::cout  << "Foo& operator= (Foo & other)    "  << (void *)&other << "/"  << (void *)other.data << "==>" ;       if (this  != &other)       {         if (NULL  != data)         {           delete  []data;           data = NULL ;         }         data = other.data;         other.data = NULL ;       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;       return  *this ;     } }; Foo func1 ()   {  Foo f1 ("abc" )  ;   return  f1; } int  main ()  {  func1();   return  0 ; } 
 
看构拷贝造函数和赋值重载,我们做了3点修改:
参数由const Foo& other变成了Foo& other。 
本对象直接偷走了other的资源(data成员),而不是重新分配。 
把other.data设置为NULL;这是为了避免析构时,同一个data被两次delete。 
 
来看看效果: 编译:
1 # g++ -fno-elide-constructors --std=c++11 Foo2.cpp -o foo2 
 
运行 foo2  输出如下:
1 2 3 4 Foo(const char * s)    0x7ffe6abae6a0/0x9f3010 Foo(Foo & other)    0x7ffe6abae6a0/0x9f3010-->0x7ffe6abae6d0/0x9f3010 ~Foo()    0x7ffe6abae6a0/0 ~Foo()    0x7ffe6abae6d0/0x9f3010 
 
输出第1行:在函数func1中,构造Foo对象f1(调用构造函数),其data在堆空间的0x9f3010处。 
输出第2行:函数func1返回时,把f1拷贝到临时对象(调用拷贝构造函数)。由于我们的修改,临时对象偷走了f1的data。f1.data变为NULL。 
输出第3行:析构f1。注意它的data已经变为0(NULL),无需调用delete。 
输出第4行:析构临时对象,delete掉它从f1偷来的data。 
 
基本符合我们的预期,不会再被访问的f1没有必要保留它的资源,所以临时对象偷走它不会带来什么负面影响。但,显然这是一个失败的尝试:
任何一个Foo的对象,只要它被别人拷贝一次(无论是拷贝构造还是赋值),它就丢失了自己的资源(data变为NULL)。对于不再被访问的对象或者临时对象,这没什么问题,但是对于其他对象,显然不可接受。 
在main函数中,若写 Foo f2 = func1();则编译失败: error: no matching function for call to ‘Foo::Foo(Foo)’。原因是func1返回的是一个右值,不能匹配类型Foo&。所以,编译器试图找签名为Foo::Foo(Foo)的构造函数。找不到导致失败。 
 
C++11的解决方案 (3) 上面的尝试注定是失败的,原因是我们把拷贝构造函数和赋值重载写成那个样子之后,一个Foo对象只要被拷贝一次(拷贝构造或者赋值)就丢失了自己的资源。这对所有的Foo对象都生效, 没有分清哪些对象可以丢失资源,哪些不行。事实上,只要分得清,这个优化的思路还是可行的。而C++11也就是这么做的。 哪些对象可以丢失自己的资源呢,换句话说,哪些对象的资源可以被move到别的对象呢?答案是:右值 。左值右值的概念在C语言以及C++11之前一直存在,一个比较严格的区分方式是:若可以进行&运算则为左值,否则为右值。但在C++11中,右值又有了新的意义:它们的资源可被move走,自己剩下一个空壳子 。所幸的是,这只是理解右值另一个角度而已 :左值右值的划分和C++11出现之前是一致的——可以进行&运算的为左值,否则为右值。但是,在C++11中更复杂,出现了五个名词:lvalue, xvalue, prvalue, glvalue, rvalue,我将在C++11中的值的类型 中进行解释,这里我们先按原来的方式找出一些右值:字面量、临时对象(编译器把这两种都叫temporary: 1.函数返回的临时对象; 2.形如Foo(“abc”)的匿名对象)等等。 还有一些左值,可以被强制move:编译器或者程序员知道它们不会再被访问了。
总结一下,可以被move的值:
右值;例如:前文中介绍的临时对象(函数返回时的那个)。 
左值,但编译器知道它不会再被访问了,隐式地把它强制为右值;例如:前面代码中,函数func1的内部变量f1。 
左值,程序员知道它不会再被访问了,显式地把它强制转换为右值; 
 
有了可被move的对象,C++11通过移动语义 来解决前述性能问题:可以为一个类定义移动拷贝构造函数 和移动赋值重载函数 ,拿Foo来举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Foo(Foo && other) noexcept  {   std ::cout  << "Foo(Foo && other)    "  << (void *)&other << "/"  << (void *)other.data << "-->" ;   data = other.data;   other.data = NULL ;   std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ; } Foo& operator = (Foo && other) noexcept  {   std ::cout  << "Foo& operator= (Foo && other)    "  << (void *)&other << "/"  << (void *)other.data << "==>" ;   if (this  != &other)   {               if (data != NULL )     {       delete  []data;       data = NULL ;     }     data = other.data;     other.data = NULL ;   }   std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;   return  *this ; } 
 
Foo && 是什么鬼?它就是右值引用 ,是C++11引入的一个新类型,记住,它是一个新类型。这个类型的值:
可以引用(或者叫绑定)右值。 
不能引用(或者叫绑定)左值,但是可以通过强制的办法(std::move)达到。 
本身是一个左值(就和之前熟悉的引用&一样),我们可以对它取地址,得到的是被引用的值的地址(就和之前熟悉的引用&一样)。 
 
在移动拷贝构造函数和移动赋值重载函数中,我们按照之前的优化思路,偷走other的资源。好,现在有了两种拷贝构造函数和两种赋值重载函数,只要分清什么时候调用哪个就行了。对应前文,可以调用移动构造或移动赋值重载的情形有3种:
右值;例如:前文中介绍的临时对象(函数返回时的那个)。若它被拷贝时,自动匹配移动拷贝或移动赋值函数;
 
左值,但编译器知道它不会再被访问了;例如:前面代码中,函数func1的内部变量f1。若它被拷贝时,编译器自动将它强制为右值引用,匹配移动拷贝或移动赋值函数;
 
左值,程序员知道它不会再被访问了;被拷贝时,程序员自己调用std::move来显式地强制为右值引用,匹配移动拷贝或者移动赋值函数。
看看我们的新版本:
 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 #include  <iostream>  #include  <string.h>  class  Foo {   private :     char  * data;   public :     virtual  ~Foo()     {       std ::cout  << "~Foo()    "  << (void *)this  << "/"  << (void *)data << std ::endl ;       if (data != NULL )       {         delete  []data;         data = NULL ;       }     }     Foo(const  char  * s) : data(NULL )     {       std ::cout  << "Foo(const char * s)    " ;       if (s)       {         int  len = strlen (s);         data = new  char [len+1 ];         memcpy (data, s, len+1 );       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;     }     Foo(const  Foo & other) : data(NULL )     {       std ::cout  << "Foo(const Foo & other)    "  << (void *)&other << "/"  << (void *)other.data << "-->" ;       if (other.data)       {         int  len = strlen (other.data);         data = new  char [len+1 ];         memcpy (data, other.data, len+1 );       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;     }     Foo& operator = (const  Foo & other)     {       std ::cout  << "Foo& operator= (const Foo & other)    "  << (void *)&other << "/"  << (void *)other.data << "==>" ;       if (this  != &other)       {         if (NULL  != data)         {           delete  []data;           data = NULL ;         }         if (NULL  != other.data)         {           int  len = strlen (other.data);           data = new  char [len+1 ];           memcpy (data, other.data, len+1 );         }       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;       return  *this ;     }     Foo(Foo && other) noexcept      {       std ::cout  << "Foo(Foo && other)    "  << (void *)&other << "/"  << (void *)other.data << "-->" ;       data = other.data;       other.data = NULL ;       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;     }     Foo& operator = (Foo && other) noexcept      {       std ::cout  << "Foo& operator= (Foo && other)    "  << (void *)&other << "/"  << (void *)other.data << "==>" ;       if (this  != &other)       {                           if (data != NULL )         {           delete  []data;           data = NULL ;         }         data = other.data;         other.data = NULL ;       }       std ::cout  << (void *)this  << "/"  << (void *)data << std ::endl ;       return  *this ;     } }; Foo func1 ()   {  Foo f1 ("abc" )  ;   return  f1; } int  main ()  {  Foo f2 = func1();   Foo f3 (f2)  ;   Foo f4 (std ::move(f3))  ;   return  0 ; } 
 
编译:
1 # g++ -fno-elide-constructors --std=c++11 Foo3.cpp -o foo3 
 
运行 foo3 ,输出如下:
1 2 3 4 5 6 7 8 9 10 Foo(const char * s)    0x7ffdb30fc3f0/0xdc8010 Foo(Foo && other)    0x7ffdb30fc3f0/0xdc8010-->0x7ffdb30fc440/0xdc8010 ~Foo()    0x7ffdb30fc3f0/0 Foo(Foo && other)    0x7ffdb30fc440/0xdc8010-->0x7ffdb30fc430/0xdc8010 ~Foo()    0x7ffdb30fc440/0 Foo(const Foo & other)    0x7ffdb30fc430/0xdc8010-->0x7ffdb30fc420/0xdc8030 Foo(Foo && other)    0x7ffdb30fc420/0xdc8030-->0x7ffdb30fc410/0xdc8030 ~Foo()    0x7ffdb30fc410/0xdc8030 ~Foo()    0x7ffdb30fc420/0 ~Foo()    0x7ffdb30fc430/0xdc8010 
 
输出第1行:构造f1。 
输出第2行:构造临时对象。f1的data被move到临时对象(通过移动拷贝构造函数)。 
输出第3行:析构f1。data已经被move,故为0(NULL)。 
输出第4行:构造f2。临时对象的data被move到f2(通过移动拷贝构造函数)。 
输出第5行:析构临时对象。data已经被move,故为0(NULL)。 
输出第6行:构造f3。由于参数是一个左值,我们有没有强制转化为右值引用,所以调用普通的构造函数,重新分配data。这时候,f2和f3都有自己的data。 
输出第7行:构造f4。f3是一个左值,但是我们把它强制转化为右值引用,所以调用移动拷贝构造函数。 
输出第8行:析构f4。 
输出第9行:析构f3。data已经被move,故为0(NULL)。 
输出第10行:析构f2。 
 
可见,move语义的引入,解决了上文提出的深拷贝新能问题。
移动构造移动赋值和异常 (4) 当你写移动构造和移动赋值函数的时候,要这样做:
尽量保证它们不抛出异常。这也是合理的,因为移动构造和移动赋值函数一般不分配内存,仅仅交换指针而已,不会抛异常; 
然后,为移动构造和移动赋值函数加上noexcept修饰符; 
 
如果不这样做,会有一种情况,你希望使用移动语义,但它却不使用;这种情况还比较常见,它就是:当std::vector自动resize的时候,你当然希望拷贝老对象到新分配的空间的时候,使用移动语义,但是它却不使用(为什么?)。
小结 (5) 本文通过示例,讲述了move产生的背后动机,并引入了右值引用的概念。有关右值引用,我将在C++11中的右值引用 中介绍。