C++复习

重新理解C11的右值引用

2018-06-08  本文已影响8人  凉拌姨妈好吃

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是左值
2.3 C11引用折叠的规则
  1. 所有右值引用叠加到右值引用上仍然是一个右值引用
  2. 所有其他引用的叠加都是左值引用
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是同一个指针,这就是指针悬挂。

如何解决指针悬挂问题

  1. 自定义深拷贝构造函数
  2. 右值引用(更优,因为不存在额外的性能损失)

可以看出下面的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
上一篇下一篇

猜你喜欢

热点阅读