• C11性能之道:右值引用


    1、左值与右值

      C++11中新增了一种类型,右值引用,标记为T &&。

      首先来介绍什么是左值和右值,左值是指表达式结束后依旧存在的持久对象,而右值是指表达式结束之后就不再存在的临时对象。一个区分左值与右值的简单方法就是:

      能不能对表达式取值,如果能,则是左值,否则为右值。所有具名变量或对象都是左值,右值不具名。

      其中,右值又有两个概念,将亡值和纯右值。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda表达式都是纯右值。将亡值在C++11中引进,将要移动的对象、T &&函数返回值、std::move返回值和转换为T &&的类型转换函数的返回值都是将亡值。

    2、&&的特性

      右值引用是对一个右值进行引用,因为右值不具名,所以只能通过引用的方式找到。右值引用不拥有绑定对象的内存,所以必须立即初始化,右值引用其实就是延长右值临时变量的生命周期,只要该右值引用变量存在,该临时右值变量则会一直存在。

    #include <iostream>
    
    using namespace std;
    
    int g_construct_count = 0;
    int g_copyconstruct_count = 0;
    int g_destruct_count = 0;
    
    struct A
    {
        A()
        {
            cout << "construct:" << ++g_construct_count << endl;
        }
        
        A(const A &a)
        {
            cout << "copyconstruct:" << ++g_copyconstruct_count << endl;
        }
        
        ~A()
        {
            cout << "destruct:" << ++g_destruct_count << endl;
        }
    };
    
    A GetA()
    {
        return A();
    }
    
    int main()
    {
        A a = GetA();
    
        return 0;
    }

      当GCC使用编译参数-fno-elide-constructors关闭返回值优化结果,会有以下输出结果:

    construct:1
    copyconstruct:1
    destruct:1
    copyconstruct:2
    destruct:2
    destruct:3

      如果开起返回值优化,那么结果如下:

    construct:1
    destruct:1

      这种优化是来自编译器优化,并不是C++标准,但是我们可以通过&&来做优化,也就是右值引用,当我们开起-fno-elide-constructors,再来看结果:

    //调用代码
    int main()
    {
        A &&a = GetA();
    
        return 0;
    }
    
    //执行结果:
    construct:1
    copyconstruct:1
    destruct:1
    destruct:2

      通过右值引用,比之前要少一次拷贝构造和析构,也就是在A &&a = GetA();的时候发生的,在这个时候使临时变量的生命周期延长。
      其实不用C++11,使用C++98/03同样可以达到以上效果,将代码改成const A& a = GetA(),常量左值引用可以接收左值、右值、常量左值和常量右值,但是普通的左值引用不接受右值,如A& a = GetA()会编译不过,非常量左值只接受左值。
      T&&并不是表示右值,有可能表示左值,也有可能表示右值。但是T &&必须被初始化,被右值初始化就是右值,被左值初始化就是左值。

    template<typename T>
    void f(T && param);
    
    f(10);   //10是右值
    int x = 10;
    f(x);    //x是左值

      当有自动推导的时候(模板自动推导,auto),&&是一个未定义的引用类型。

    template<typename T>
    void f(T&& param);      //类型需要推导,&&是一个未定义引用类型
    
    template<typename T>
    class Test
    {
       Test(Test && ths);     //右值,定义特定类型,没有类型推断
    }
    
    void f(Test && param);    //右值,定义确定类型,没有类型推断
    
    template<typename T>
    void f(std::vector<T> && param); //右值,vector<T>已经确定类型
    
    template<typename T>
    void f(const T && param);        //右值,加上const修饰改变未定义引用

      当未定义引用类型仅在T&&的时候有效,任何附加条件都会使其失效变成一个普通的右值引用。这种类型变化在C++11中成为折叠引用,其规则如下:

    •   所有的右值引用叠加到右值引用还是右值引用;
    •   所有的其他引用类型之间的叠加都变成左值引用。
    int &&var1 = x;         //var1->int &&,右值引用
    auto &&var2 = var1;        //var2存在类型推导,未定义引用类型,var2->int &,左值引用
    
    int i1,i2;
    auto && v1 = i1;          //左值,被左值初始化
    decltype(i1) && v2 = i2;     //error,右值引用不能被左值初始化

      对于左值初始化右值引用可以使用std::move;

    decltype(i1) && v2 = std::move(i2);

      编译器会将已经命名的右值引用视为左值,未命名的右值视为右值。

    #include <iostream>
    
    using namespace std;
    
    int g_lvalue = 0;
    int g_rvalue = 0;
    
    void PrintValue(int &i)
    {
        cout << "Lvalue:" << i << endl;
    }
    
    void PrintValue(int &&i)
    {
        cout << "Rvalue:" <<  i << endl;
    }
    
    void Forward(int &&i)
    {
        PrintValue(i);  //转发之后变成左值,右值变成一个命名对象,编译器当成左值
    }
    
    int main()
    {
        int i = 0;
        PrintValue(i);
        PrintValue(10);
        Forward(2);
    
        return 0;
    }

      执行结果:

    Lvalue:0
    Rvalue:10
    Lvalue:2

      总结:
      左值引用和右值引用独立于他们的类型,右值引用类型可能值左值也可能是右值;
      auto &&或函数参数类型自动推导的T &&是一个未定的引用类型,可能是左值引用也可能是右值引用,取决于初始化类型;
      所有的右值引用叠加到右值引用是一个右值引用,其它类型都为左值引用;
      编译器会将已命名的右值引用视为左值,未命名的右值引用视为右值。

    2、右值引用,避免深拷贝

      当一个类含有堆内存,我们需要提供深拷贝的拷贝函数,如果使用默认构造函数会出现内存的重复删除。

    #include <iostream>
    
    class A
    {
    public:
        A() :m_ptr(new int(0)){}
        
        ~A()
        {
            delete m_ptr;
        }
    private:
        int *m_ptr;
    };
    
    int main()
    {
        A a;
        A b;
        
        b = a; //运行出错
        return 0;
    }

      上述例子中,a和b指向统一指针m_ptr,析构时重复删除该指针。正确的做法应该是提供深拷贝函数。

    #include <iostream>
    
    using namespace std;
    
    class A
    {
    public:
        A() :m_ptr(new int(0))
        {
            cout << "construct" << endl;
        }
        
        ~A()
        {
            cout << "destruct" << endl;
            delete m_ptr;
        }
        
        A(const A & a):m_ptr(new int(*a.m_ptr))
        {
            cout << "copy construct" << endl;
        }
        
    private:
        int *m_ptr;
    };
    
    A GetA()
    {
        A a;
        
        return a;
    }
    
    int main()
    {
        A a = GetA();
        return 0;
    }
    
    //运行结果:
    construct
    copy construct
    destruct
    copy construct
    destruct
    destruct

      虽然可以解决问题,但是多次的拷贝确实不必要的,临时变量拷贝完就删除了,如果堆内存黑大,拷贝的代价就会很大。

    #include <iostream>
    
    using namespace std;
    
    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(A && a):m_ptr(a.m_ptr)
        {
            a.m_ptr = nullptr;
            cout << "move construct" << endl;
        }
        
        ~A()
        {
            cout << "destruct" << endl;
            delete m_ptr;
        }
        
    private:
        int *m_ptr;
    };
    
    A GetA()
    {
        A a;
        
        return a;
    }
    
    int main()
    {
        A a = GetA();
        return 0;
    }
    
    //运行结果:
    construct
    move construct
    destruct
    move construct
    destruct
    destruct

      很明显,减少了一次拷贝构造,取而代之的是移动构造,在内存方面没有变化,避免了临时对象的深拷贝,提升了性能。这里A &&根据参数是左值还是右值来建立分支,如果是临时值,则会使用移动构造函数。
      移动语义可以将资源(堆、系统对象)通过浅拷贝方式从一个对象转移到另一个对象,减少不必要的临时对象的创建、拷贝和销毁,大幅提高性能。

  • 相关阅读:
    关键词提取算法TextRank
    我的博文目录整理
    Windows Azure一些小技巧集合
    js数组和树互转
    this.props.form.validateFields回调不执行问题
    d3的4.x与3.x版本的区别
    d3提示框,虚线,选择区域
    d3布局
    d3文件导入和导出
    d3交互
  • 原文地址:https://www.cnblogs.com/ChinaHook/p/7684122.html
Copyright © 2020-2023  润新知