zoukankan      html  css  js  c++  java
  • 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 &&根据参数是左值还是右值来建立分支,如果是临时值,则会使用移动构造函数。
      移动语义可以将资源(堆、系统对象)通过浅拷贝方式从一个对象转移到另一个对象,减少不必要的临时对象的创建、拷贝和销毁,大幅提高性能。

  • 相关阅读:
    洛谷p1017 进制转换(2000noip提高组)
    Personal Training of RDC
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Eurasia
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Peterhof.
    Asia Hong Kong Regional Contest 2019
    XVIII Open Cup named after E.V. Pankratiev. Grand Prix of Siberia
    XVIII Open Cup named after E.V. Pankratiev. Ukrainian Grand Prix.
    XVIII Open Cup named after E.V. Pankratiev. GP of SPb
    卜题仓库
    2014 ACM-ICPC Vietnam National First Round
  • 原文地址:https://www.cnblogs.com/ChinaHook/p/7684122.html
Copyright © 2011-2022 走看看