c++新特性之右值引用和移动语义

目录

前言

一、什么是左值、右值?

二、什么是左值引用、右值引用

1.左值引用

2.右值引用

3 对左右值引用本质的讨论

3.1 右值引用有办法指向左值吗?

3.2 左值引用、右值引用本身是左值还是右值?

4、右值引用和std::move使用场景

4.1 func(const T&)优化性能,避免浅拷贝

4.2 移动(move )语义

4.3 forward 完美转发

4.4 emplace_back 减少内存拷贝和移动 

总结


前言

C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。

一、什么是左值、右值

可以从2个角度判断:

  • 左值可以取地址、位于等号左边
  • 而右值没法取地址,位于等号右边
// 案例 1
int a = 6 ;
a 可以通过 & 取地址,位于等号左边,所以 a 是左值。
6 位于等号右边, 6 没法通过 & 取地址,所以 6 是个右值。

再举个复杂点的例子:

// 案例 2
struct A {
        A ( int a = 0 ) {
                a_ = a ;
        }
        int a_ ;
};
A a = A ();
同样的, a 可以通过 & 取地址,位于等号左边, 所以 a 是左值
A() 是个临时值,没法通过 & 取地址,位于等号右边, 所以 A() 是个右值
可见左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。

二、什么是左值引用、右值引用

引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝。

1.左值引用

左值引用:能指向左值,不能指向右值的就是左值引用。

代码如下(示例):

// 示例 1
int a = 5 ;
int & ref_a = a ; // 左值引用指向左值,编译通过
int & ref_a = 5 ; // 左值引用指向了右值,会编译失败
// 示例 2 ( const左值引用是可以指向右值的)
const int & ref_a = 5 ; // 编译通过,const左值引用不会修改指向值
// 实例3 ( const & 作为函数参数
void push_back ( const value_type & val ); // 编译通过,const左值引用不会修改指向值
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。

2.右值引用

再看下右值引用,右值引用的标志是 && ,顾名思义,右值引用专门为右值而生, 可以指向右值,不能指 向左值

代码如下(示例):

int && ref_a_right = 5 ; // ok
int a = 5 ;
int && ref_a_left = a ; // 编译不过,右值引用不可以指向左值
ref_a_right = 6 ; // 右值引用的用途:可以修改右值

3 对左右值引用本质的讨论

左右值引用的本质。

3.1 右值引用有办法指向左值吗?

有办法,使用 std::move
代码如下(示例):
int a = 5 ; // a 是个左值
int & ref_a_left = a ; // 左值引用指向左值
int && ref_a_right = std::move ( a ); // 通过 std::move 将左值转化为右值,可以被右值引用指向
cout << a ; // 打印结果: 5
  • 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量;
  • 但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换: static_cast<T&&>(lvalue) 。 所以,单纯的std::move(xxx) 不会有性能提升
同样的, 右值引用能指向右值 ,本质上也是把右值提升为一个左值,并定义一个右值引用通过 std::move 指向该左值:
int && ref_a = 5 ;
ref_a = 6 ;
等同于以下代码:
int temp = 5 ;
int && ref_a = std::move ( temp );  // 此时 temp 等于右值
ref_a = 6 ;

3.2 左值引用、右值引用本身是左值还是右值?

  • 被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。仔细看下边代码:

 // 形参是个右值引用

void change ( int && right_value ) {
        right_value = 8 ;
}
int main () {
        int a = 5 ; // a 是个左值
        int & ref_a_left = a ; // ref_a_left 是个左值引用
        int && ref_a_right = std::move ( a ); // ref_a_right 是个右值引用
        change ( a ); // 编译不过, a 是左值, change 参数要求右值
        change ( ref_a_left ); // 编译不过,左值引用 ref_a_left 本身也是个左值
        change ( ref_a_right ); // 编译不过,右值引用 ref_a_right 本身也是个左值
        change ( std::move ( a )); // 编译通过
        change ( std::move ( ref_a_right )); // 编译通过
        change ( std::move ( ref_a_left )); // 编译通过
        change ( 5 ); // 当然可以直接接右值,编译通过
        
         // 打印下面三个左值的地址,都是一样的
        cout << & a << ' ' ;
        cout << & ref_a_left << ' ' ;
        cout << & ref_a_right ;    
}
  • 作为函数返回值的 && 是右值,直接声明出来的 && 是左值 
结论
  • 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  • 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const值引用也能指向右值)
  • 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

4、右值引用和std::move使用场景

std::move 只是类型转换工具,不会对性能有好处;

右值引用在作为函数形参时更具灵活性。他们有什么实际应用场景吗?

4.1 func(const T&)优化性能,避免浅拷贝

浅拷贝重复释放

        对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:

#include <iostream>
using namespace std;
class A
{
public:
    A() :m_ptr(new int(0)) {
        cout << "constructor A" << endl;
    }
    ~A(){
        cout << "destructor A, m_ptr:" << m_ptr << endl;
        delete m_ptr;
        m_ptr = nullptr;
    }
private:
    int* m_ptr;
};

// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
    }
int main()
{
    {
        A a = Get(false); // 运行报错
    }
    cout << "main finish" << endl;
    return 0;
}

// 上面代码运行结果

constructor A
constructor A
ready return
destructor A, m_ptr: 0xf87af8
destructor A, m_ptr:0xf87ae8
destructor A, m_ptr: 0xf87af8
main finish
深拷贝构造函数
        在上面的代码中,默认构造函数是浅拷贝,main 函数的 a Get 函数的 b 会指向同一个指针 m_ptr ,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码:
#include <iostream>
using namespace std;
class A
{
public:
    A() :m_ptr(new int(0)) {
        cout << "constructor A" << endl;
    }
    A(const A& a) :m_ptr(new int(*a.m_ptr)) {
        cout << "copy constructor A" << endl;
    }
    ~A(){
        cout << "destructor A, m_ptr:" << m_ptr << endl;
        delete m_ptr;
        m_ptr = nullptr;
    }
private:
    int* m_ptr;
};

// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
}
int main()
{
    {
        A a = Get(false); // 正确运行
    }
    cout << "main finish" << endl;
    return 0;
}
// 上面代码 运行结果
constructor A
constructor A
ready return
copy constructor A
destructor A, m_ptr:0xea7af8
destructor A, m_ptr:0xea7ae8
destructor A, m_ptr:0xea7b08
main finish

移动构造函数 

        这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:

#include <iostream>
using namespace std;
class A
{
public:
    A() :m_ptr(new int(0)) {
        cout << "constructor A" << endl;
    }
    A(const A& a) :m_ptr(new int(*a.m_ptr)) {
        cout << "copy constructor A" << endl;
    }
    // 移动构造函数,可以浅拷贝
    A(A&& a) :m_ptr(a.m_ptr) {
        a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
        cout << "move constructor A" << endl;
    }
    ~A(){
        cout << "destructor A, m_ptr:" << m_ptr << endl;
        if(m_ptr)
        delete m_ptr;
    }
private:
    int* m_ptr;
};

// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
    }
int main()
{
    {
        A a = Get(false); // 正确运行
    }
    cout << "main finish" << endl;
    return 0;
}
// 上面代码 运行结果
constructor A
constructor A
ready return
move constructor A
destructor A, m_ptr:0
destructor A, m_ptr:0xfa7ae8
destructor A, m_ptr:0xfa7af8
main finish
        上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct )。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&& ,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义), 右值引用的一个重 要目的是用来支持移动语义的。
        移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少 不必要的临时对象的创建 、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

4.2 移动(move )语义

        move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要 move 语义起作用,核心在于需要对应类型的构造函数支持。
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;
class MyString {
private:
    char* m_data;
    size_t m_len;
    void copy_data(const char *s) {
        m_data = new char[m_len+1];
        memcpy(m_data, s, m_len);
        m_data[m_len] = '\0';
    }
public:
    MyString() {
        m_data = NULL;
        m_len = 0;
    }
    MyString(const char* p) {
        m_len = strlen (p);
        copy_data(p);
    }
    MyString(const MyString& str) {
        m_len = str.m_len;
        copy_data(str.m_data);
        std::cout << "Copy Constructor is called! source: " << str.m_data <<
        std::endl;
    }
    MyString& operator=(const MyString& str) {
        if (this != &str) {
            m_len = str.m_len;
            copy_data(str.m_data);
        }
        std::cout << "Copy Assignment is called! source: " << str.m_data <<
        std::endl;
        return *this;
    }
    
    // 用c++11的右值引用来定义这两个函数
    MyString(MyString&& str) {
        std::cout << "Move Constructor is called! source: " << str.m_data <<
        std::endl;
        m_len = str.m_len;
        m_data = str.m_data; //避免了不必要的拷贝
        str.m_len = 0;
        str.m_data = NULL;
    }
    MyString& operator=(MyString&& str) {
        std::cout << "Move Assignment is called! source: " << str.m_data <<
        std::endl;
        if (this != &str) {
            m_len = str.m_len;
            m_data = str.m_data; //避免了不必要的拷贝
            str.m_len = 0;
            str.m_data = NULL;
        }
        return *this;
    }
    virtual ~MyString() {
        if (m_data) free(m_data);
    }
};

int main()
{
    MyString a;
    a = MyString("Hello"); // Move Assignment
    MyString b = a; // Copy Constructor
    MyString c = std::move(a); // Move Constructor is called! 将左值转为右值
    std::vector<MyString> vec;
    vec.push_back(MyString("World")); // Move Constructor is called!
    return 0;
}
        有了func(const T&)引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。

4.3 forward 完美转发

        forward 完美转发实现了 参数在传递过程中保持其值属性的功能 ,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
现存在一个函数
Template < class T >
void func ( T && val );
根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后, 这个 val 值它本质上是一个左值!

看下面例子

int && a = 10 ;
int && b = a ; // 错误
int && b = std::forward < int > ( a ); //正确

4.4 emplace_back 减少内存拷贝和移动 

对于STL容器,C++11后引入了emplace_back接口,emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。

考虑这样的语句:

vector < string > testVec ;
testVec . push_back ( string ( 16 , 'a' ));

上述语句足够简单易懂,将一个string对象添加到testVec中。底层实现:

  • 首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。
  • 其次,vector内会创建一个新的string对象,这是第二次构造。
  • 最后在push_back结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次 string构造和一次析构。

c++11 可以用 emplace_back 代替 push_back emplace_back 可以直接在 vector 中构建一个对象,而非创建一个临时对象,再放进vector ,再销毁。 emplace_back 可以省略一次构建和一次析构,从而达到优化的目的。

总结

C++11 在性能上做了很大的改进,最大程度减少了内存移动和复制,通过右值引用、 forward
emplace 和一些无序容器我们可以大幅度改进程序性能。
右值引用仅仅是通过改变资源的所有者来避免内存的拷贝,能大幅度提高性能。
forward 能根据参数的实际类型转发给正确的函数。
emplace 系列函数通过直接构造对象的方式避免了内存的拷贝和移动。
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值