C++11 新特性及原理(二、右值引用及移动构造函数)
目录
接上篇C++11 新特性及原理(一、基础篇)文章继续,该篇文章内容较多请读者耐心去品味,定能掌握右值引用及移动构造函数的精髓,在以后的代码开发中将性能提高到极致。
四、右值引用及移动构造函数(提高性能)
1、右值引用及右值的概念
右值:通常是指临时的对象,它们只在当前的语句中有效。
右值引用 (Rvalue Referene) 是 C++11引入的新特性,它是用来支持转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:
1、消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
2、能够更简洁明确地定义泛型函数。
有了右值引用后,我们就能对右值进行修改与操作,看下列示例 :
void AddValue(int& i) {
i+=2;
std::cout << "LValue AddValue: " << i << std::endl;
}
void AddValue(int&& i) {
i+=10;
std::cout << "RValue AddValue: " << i << std::endl;
}
void ForwardValue(int&& i) {
AddValue(i);
}
int main() {
int a = 1;
AddValue(a); // LValue AddValue: 3
AddValue(1); // RValue AddValue: 11
ForwardValue(2); // LValue AddValue: 4
}
虽然 2 这个立即数在函数 forward_value 接收时是右值,但到了 process_value 接收时,变成了左值。
2、move语义
左值(引用)可以通过std::move()强制类型转换成右值引用。
template<typename _Tp>
inline typename std::remove_reference<_Tp>::type&& move(_Tp&& __t) {
return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}
以上是std::move()在头文件中的实现,其本质就是一个模板函数,只是将参数做类型强转换为右值引用。 所以,它并未做任何实质的动作。std::move的作用只是为了让调用构造函数的时候告诉编译器去选择移动构造函数。
3、移动构造函数
Move constructor |
MyClass(MyClass&& other) noexcept; |
Move assignment operator |
MyClass& operator=(MyClass&& other) noexcept; |
有了以上两个支持后,移动构造函数才能将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。在生活中有很多这样的例子,我们将钱从一个银行卡转移到另一个银行卡,将手机SIM卡转移到另一个手机中,把一段文字从一个位置剪切到另一个位置等等,移动构造可以减少不必要的复制,带来性能上的提升。有些复制构造是必要的,我们确实需要另外一个副本;而有些复制构造是不必要的,我们可能只是希望这个对象换个地方,移动一下而已。在C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。而现在在某些情况下,我们没有必要复制对象——只需要移动它们。
#include <string.h>
#include <iostream>
using namespace std;
class IntArr
{
public:
IntArr(int x = 10):m_ptr(new int[x]), m_cot(x) {
printf("%p:IntArr(int x) ptr: %p[%d]\n", this, m_ptr, m_cot);
}
~IntArr() {
printf("%p:~IntArr()\n", this);
delete[] m_ptr;
}
IntArr(IntArr&& n) noexcept {
m_cot = n.m_cot;
m_ptr = n.m_ptr;
n.m_cot = 0;
n.m_ptr = nullptr;
printf("%p:IntArr(IntArr&& n) ptr: %p[%d]\n", this, m_ptr, m_cot);
}
IntArr(const IntArr& n) {
m_cot = n.m_cot;
m_ptr = new int[m_cot];
memcpy(m_ptr, n.m_ptr, sizeof(int) * m_cot);
printf("%p:IntArr(IntArr& n) ptr: %p[%d]\n", this, m_ptr, m_cot);
}
void ShowInfo(const char* pObj) {
printf("%p:Object %s ptr: %p[%d]\n", this, pObj, m_ptr, m_cot);
}
private:
int m_cot;
int *m_ptr;
};
//返回值为IntArr类对象
IntArr getNum(){
IntArr a(128);
a.ShowInfo("a");
return a;
}
int main() {
IntArr c(getNum());
c.ShowInfo("c");
printf("================================\n");
IntArr b(IntArr(256));
b.ShowInfo("b");
printf("================================\n");
IntArr f = std::move(b);
f.ShowInfo("f");
return 0;
}
默认编译的执行结果:
0x7ffd6f624060:IntArr(int x) ptr: 0x603010[128]
0x7ffd6f624060:Object a ptr: 0x603010[128]
0x7ffd6f624060:Object c ptr: 0x603010[128]
================================
0x7ffd6f624050:IntArr(int x) ptr: 0x603220[256]
0x7ffd6f624050:Object b ptr: 0x603220[256]
================================
0x7ffd6f624040:IntArr(IntArr&& n) ptr: 0x603220[256]
0x7ffd6f624040:Object f ptr: 0x603220[256]
0x7ffd6f624040:~IntArr()
0x7ffd6f624050:~IntArr()
0x7ffd6f624060:~IntArr()
虽然编译的时候没有指定任何-O的优化选项,gcc实际上有默认的优化行为的。即使是-O0也有。gcc4.4以上支持--help=optimizer这样的帮助选项。gcc -Q -O0 --help=optimizers|grep enabled|wc,当前使用的gcc4.8.5有52个优化选项。换句话说,编译器总是尽可能的会为我们优化掉不必要代码,减少不必要的执行指令。我们看下编译器为我们优化了哪些代码(以上根据输出推论编译器的代码优化)。由于编译器的优化会让我们误导的认为只有显示使用move语义时才会调用移动构造函数,对我们理解代码本身发生了什么造成了一定的影响,为了避免这种本质上的改变,我们在编译时去掉构造函数的优化加入编译选项:
g++ -std=c++11 -fno-elide-constructors my_code.cpp
增加编译选项-fno-elide-constructors 后的执行结果:
0x7ffeca5a9be0:IntArr(int x) ptr: 0x603010[128]
0x7ffeca5a9be0:Object a ptr: 0x603010[128]
0x7ffeca5a9c40:IntArr(IntArr&& n) ptr: 0x603010[128]
0x7ffeca5a9be0:~IntArr()
0x7ffeca5a9c30:IntArr(IntArr&& n) ptr: 0x603010[128]
0x7ffeca5a9c40:~IntArr()
0x7ffeca5a9c30:Object c ptr: 0x603010[128]
================================
0x7ffeca5a9c50:IntArr(int x) ptr: 0x603220[256]
0x7ffeca5a9c20:IntArr(IntArr&& n) ptr: 0x603220[256]
0x7ffeca5a9c50:~IntArr()
0x7ffeca5a9c20:Object b ptr: 0x603220[256]
================================
0x7ffeca5a9c10:IntArr(IntArr&& n) ptr: 0x603220[256]
0x7ffeca5a9c10:Object f ptr: 0x603220[256]
0x7ffeca5a9c10:~IntArr()
0x7ffeca5a9c20:~IntArr()
0x7ffeca5a9c30:~IntArr()
到底调用了copy还是move?
在没有移动构造函数时,50和54行代码,会调用我们实现好的copy构造函数,c++11后会调用移动构造函数,调用的原因还是根据参数类型所决定的,移动构造的参数一定是右值语义。
需要注意的问题:构造函数有拷贝构造的实现而没有移动构造的实现时,假如使用了显示调用std::move()语义,这时会调用拷贝构造函数。