1. 右值
- 左值:既能出现在等号左边也能出现在等号右边的变量或表达式
- 右值:因为声明结束后会被销毁,所以不能放在等号左边
int a = 3;
int b = 2; //此时a b都是左值
int c = a+b;//此时a+b就是右值,a+b产生了临时的变量,在表达式结束之后就会被销毁
c中,左值其实就是有名字的变量,而运算操作(加减乘除,函数返回值等)就是右值,右值不允许被修改
c++中,相对于右值引入了一个新的概念,基础类型右值不允许被修改,但用户自定义的类型,右值可以通过它的成员函数进行修改。
这里要引入一个C11的新概念:C11要求所有的值只能为下面三种:左值、将亡值、纯右值。左值和右值都介绍过了,现在来解释一下什么是将亡值:将要被移动的对象、T&&函数的返回值、std::move返回值等
2. 右值引用
2.1 类的右值引用的好处
类的右值是一个临时对象,如果没有被绑定到引用,在表达式结束时就会被废弃。于是我们可以在右值被废弃之前,移走它的资源进行废物利用,从而避免无意义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会降低。
总结来说:1. 避免无意义复制 2.析构开销减少
2.2 右值引用的特点
- 延长了右值的生命周期
int main() {
A&& a = GetA();
return 0;
}
GetA()被绑定在右值引用,所以生命周期被延长,减少了临时对象的拷贝和析构。
- 右值引用 引用右值也引用左值
template<typename T>
void f(T&& t){}
f(10); //t是右值
int x = 10;
f(x); //t是左值
- 当右值引用T&& t在发生自动类型判断的时候,传给它的是右值,那么它就是一个右值,如果传入的是一个左值,那么它就是一个左值。
2.3 C11引用折叠的规则
- 所有右值引用叠加到右值引用上仍然是一个右值引用
- 所有其他引用的叠加都是左值引用
2.4 类的构造函数的形参使用右值引用的好处
首先思考一下指针悬挂出现的条件:
当类没有定义构造函数,我们实例化类的时候,会使用默认构造函数。如果我们类某个属性需要动态分配内存,那么这个属性很可能会被多次删除。
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;
}
如果我们在上面没有使用深拷贝构造函数,而是使用了系统默认的构造函数,那么类内部的m_ptr将会被删除两次,一次是临时右值析构的时候删除一次,第二次外面构造的a对象释放时删除一次,而这两个对象的m_ptr是同一个指针,这就是指针悬挂。
如何解决指针悬挂问题
- 自定义深拷贝构造函数
- 右值引用(更优,因为不存在额外的性能损失)
可以看出下面的A(A&& a)的构造函数其实就是一个移动构造函数。
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);
}
如果在使用左值的时候也希望能减少拷贝来优化性能,我们可以使用std::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); //这里没有拷贝
move的源码如下
template<typename _Tp>
inline typename std::remove_reference<_Tp>::type&& move(_Tp&& __t)
{
return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}
源码中的static_cast是什么
static_cast是一个强制类型转换符,强制类型转换会告诉编译器:我们知道并且不会在意潜在的精度损失。
2.5 完美转发
有时候我们需要按照参数的原本类型进行转发,而不需要进行默认的类型转换。那么我们就可以使用完美转发std::forward,std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发
void processValue(int& a){ cout << "lvalue" << endl; }
void processValue(int&& a){ cout << "rvalue" << endl; }
template <typename T>
void forwardValue(T&& val)
{
processValue(std::forward<T>(val)); //照参数本来的类型进行转发。
}
void Testdelcl()
{
int i = 0;
forwardValue(i); //传入左值
forwardValue(0);//传入右值
}
输出:
lvaue
rvalue
网友评论