一,解决什么问题
- 解决c++98中,一个临时对象非必要的昂贵的拷贝工作,
- 在模板函数中如何按照参数的实际类型进行转发
二,左值和右值?
- 能取地址的就左值,不能取地址的就是右值
- 只能放在等号右边的就是右值,
举例
int i = 0;
- 在上面的那句话中,0就是一个右值,不能对其赋值和取地址,i是一个左值,可以放在等号的任意一边,
- 具体来说上面的表达式中等号右边的0是纯右值(prvalue),在C++11中所有的值必属于左值、将亡值、纯右值三者之一。
- 比如,非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式等都是纯右值。
- 而将亡值是C++11新增的、与右值引用相关的表达式,比如,将要被移动的对象、T&&函数返回值、std::move返回值和转换为T&&的类型的转换函数的返回值等。
三,右值的定义特点
举例
T&& k = getValue();
- 其中“&&”就是右值引用,右值引用是引用右值的,此时函数的返回值就是一个右值,这里函数的返回值是一个临时变量,只能放在等号的右边,一般来说,这个语法中,这个临时的变量在表达式结束后就会被销毁,在这里这个临时的变量不会被销毁,会因为右值引用的接收而延续生命周期,这个临时变量的生命周期和变量k的生命周期一样长,
- 这就是右值引用的特点之一:延迟将亡值的生命周期
右值引用的第一个特点
通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。让我们通过一个简单的例子来看看右值的生命周期。如代码下
#include <iostream>
using namespace std;
int g_constructCount=0;
int g_copyConstructCount=0;
int g_destructCount=0;
struct A
{
A(){
cout<<"construct: "<<++g_constructCount<<endl;
}
A(const A& a)
{
cout<<"copy construct: "<<++g_copyConstructCount <<endl;
}
~A()
{
cout<<"destruct: "<<++g_destructCount<<endl;
}
};
A GetA()
{
return A();
}
int main() {
A a = GetA();
return 0;
}
- 如果用一般的变量来接收函数的返回值,具体运行情况如下
- 如果用右值引用方式接收函数的返回值,结构如下
右值引用的第二个特点
- 右值引用独立于左值与右值
- 意思就是右值引用变量的类型可以是左值也可以是右值,
int&& val = 1;
- 此时的val是一个右值引用,但是val的本身是一个左值,1是一个右值,
右值引用会自动发生类型推演
template<typename T>
void f(T&& t){}
f(10); //t是右值
int x = 10;
f(x); //t是左值
- 函数的参数是T&& t,这表示不确定是什么类型,是未定义的引用类型,如果被左值初始化,就是左值,被右值初始化就是右值,是左值还是右值取决于初始化,但是只有发生类型推断的时候才会这样判断,
- 右值引用可能是左值可能是右值,依赖于初始化(移动语义和完美转发)
四,右值引用的应用
移动语义(移动构造函数)
- 深浅拷贝的问题
class A
{
public:
A():m_ptr(new int(0)){cout << "construct" << endl;}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数
{
cout << "copy construct" << endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
int main() {
A a = GetA();
return 0;
}
输出:
construct
copy construct
copy construct
- 这个例子很简单,一个带有堆内存的类,必须提供一个深拷贝拷贝构造函数,因为默认的拷贝构造函数是浅拷贝,会发生“指针悬挂”的问题。
- 如果不提供深拷贝的拷贝构造函数,上面的测试代码将会发生错误(编译选项-fno-elide-constructors),内部的m_ptr将会被删除两次,一次是临时右值析构的时候删除一次,第二次外面构造的a对象释放时删除一次,而这两个对象的m_ptr是同一个指针,这就是所谓的指针悬挂问题。
- 提供深拷贝的拷贝构造函数虽然可以保证正确,但是在有些时候会造成额外的性能损耗,因为有时候这种深拷贝是不必要的。比如下面的代码
class A
{
public:
A() :m_ptr(new int(0)){}
A(const A& a):m_ptr(new int(*a.m_ptr)) //深拷贝的拷贝构造函数
{
cout << "copy construct" << endl;
}
A(A&& a) :m_ptr(a.m_ptr)
{
a.m_ptr = nullptr;
cout << "move construct" << endl;
}
~A(){ delete m_ptr;}
private:
int* m_ptr;
};
int main(){
A a = Get(false);
}
输出:
construct
move construct
move construct
- 和第一段代码相比,就是多了一个构造函数,这个构造函数并没有做深拷贝,仅仅是将指针的所有者转移到了另外一个对象,同时,将参数对象a的指针置为空,这里仅仅是做了浅拷贝,因此,这个构造函数避免了临时变量的深拷贝问题。
-
移动构造函数:上面这个函数其实就是移动构造函数,他的参数是一个右值引用类型,这里的A&&表示右值,为什么?前面已经提到,这里没有发生类型推断,是确定的右值引用类型。为什么会匹配到这个构造函数?因为这个构造函数只能接受右值参数,而函数返回值是右值,所以就会匹配到这个构造函数。这里的A&&可以看作是临时值的标识,对于临时值我们仅仅需要做浅拷贝即可,无需再做深拷贝,从而解决了前面提到的临时变量拷贝构造产生的性能损失的问题。这就是所谓的移动语义,右值引用的一个重要作用是用来支持移动语义的。
- 需要注意的一个细节是,我们提供移动构造函数的同时也会提供一个拷贝构造函数,以防止移动不成功的时候还能拷贝构造,使我们的代码更安全
-
move
- 我们知道移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借助移动语义来优化性能呢,那该怎么做呢?事实上C++11为了解决这个问题,提供了std::move方法来将左值转换为右值,从而方便应用移动语义。move是将对象资源的所有权从一个对象转移到另一个对象,只是转移,没有内存的拷贝,这就是所谓的move语义
{
std::list< std::string> tokens;
//省略初始化...
std::list< std::string> t = tokens; //这里存在拷贝
}
std::list< std::string> tokens;
std::list< std::string> t = std::move(tokens); //这里没有拷贝
- 如果不用std::move,拷贝的代价很大,性能较低。使用move几乎没有任何代价,只是转换了资源的所有权。他实际上将左值变成右值引用,然后应用移动语义,调用移动构造函数,就避免了拷贝,提高了程序性能。如果一个对象内部有较大的对内存或者动态数组时,很有必要写move语义的拷贝构造函数和赋值函数,避免无谓的深拷贝,以提高性能。事实上,C++11中所有的容器都实现了移动语义,方便我们做性能优化。
- 这里也要注意对move语义的误解,move实际上它并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用。如果是一些基本类型比如int和char[10]定长数组等类型,使用move的话仍然会发生拷贝(因为没有对应的移动构造函数)。所以,move对于含资源(堆内存或句柄)的对象来说更有意义。