美文网首页C++复习
重新理解C11的右值引用

重新理解C11的右值引用

作者: 凉拌姨妈好吃 | 来源:发表于2018-06-08 00:41 被阅读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是左值
    
    • 当右值引用T&& 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
    

    相关文章

      网友评论

        本文标题:重新理解C11的右值引用

        本文链接:https://www.haomeiwen.com/subject/utnnsftx.html