c++11 右值引用和移动语义详解

右值引用若不作为函数参数使用,基本等于滥用

右值引用 (Rvalue Referene) 是 C++ 新标准 中引入的新特性 , 它实现了移动语义 (Move Sementics) 和完美转发 (Perfect Forwarding)。它的主要目的有两个方面:
  1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
  2. 能够更简洁明确地定义泛型函数。

何为右值:

   C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,
  那些可以在多条语句中使用的对象。 所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。
  右值是指临时的对象,它们只在当前的语句中有效。

int i = 0;// 在这条语句中,i 是左值,0 是临时变量,就是右值

在C++11之前,右值是不能引的,如

int &a = 1;// error : 非常量的引用必须为左值
const int &a = 1;// 我们最多只能用常量引用来绑定一个右值

在C++11中我们可以引用右值,使用&&实现:

int &&a = 1;

应用场景:

如下string类,实现了拷贝构造和赋值运算符重载

#include 
#include 
using namespace std;

class MyString
{
public:
    MyString()
    {
        m_data = NULL;
        m_len = 0;
    }

    MyString(const char* s)
    {
        m_len = strlen(s);
        init_data(s);
        cout << "构造函数" << s << endl;
    }

    MyString(const MyString& str)
    {
        m_len = str.m_len;
        init_data(str.m_data);
        cout << "拷贝构造函数" << str.m_data << endl;
    }

    MyString& operator=(const MyString& str)
    {
        if ( this != &str ){
            this->m_len = str.m_len;
            init_data(str.m_data);
        }
        cout << "等号操作符重载" << str.m_data << endl;
        return *this;
    }

    ~MyString()
    {
        if ( m_data != NULL ){
            cout << "析构函数" << endl;
            free(m_data);
        }
    }

private:
    void init_data(const char* s)
    {
        m_data = new char[m_len + 1];
        memcpy(m_data, s, m_len);
        m_data[m_len] = '\0';
    }
    char* m_data;
    size_t m_len;
};

void test()
{
    vector vec;
    MyString a;// 没有输出
    a = MyString("hello");
    vec.push_back(MyString("world"));
}

int main()
{
    test();

    system("pause");
    return 0;
}

输出:
构造函数hello
等号操作符重载hello
析构函数
构造函数world
拷贝构造函数world
析构函数
析构函数
析构函数

总共执行了2次拷贝,MyString("Hello")和MyString("World")都是临时对象,临时对象被使用完之后会被立即析构,在析构函数中free掉申请的内存资源。
如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。 这正是定义移动语义的目的。

通过加入定义移动构造函数和转移赋值操作符重载来实现右值引用(即复用临时对象):

#include 
#include 
using namespace std;

class MyString
{
public:
    MyString()
    {
        m_data = NULL;
        m_len = 0;
    }

    MyString(const char* s)
    {
        m_len = strlen(s);
        init_data(s);
        cout << "构造函数" << s << endl;
    }

    MyString(MyString&& str)
    {
        cout << "移动构造函数" << str.m_data << endl;
        m_len = str.m_len;
        m_data = str.m_data;
        str.m_len = 0;
        str.m_data = NULL;// 防止在析构函数中将内存释放掉       
    }

    MyString& operator=(MyString&& str)
    {
        cout << "移动等号操作符重载" << str.m_data << endl;
        if ( this != &str ){
            this->m_len = str.m_len;
            this->m_data = str.m_data;
            str.m_len = 0;
            str.m_data = NULL;// 防止在析构函数中将内存释放掉  
        }
        return *this;
    }

    ~MyString()
    {
        if ( m_data != NULL ){
            cout << "析构函数" << endl;
            free(m_data);
        }
    }

private:
    void init_data(const char* s)
    {
        m_data = new char[m_len + 1];
        memcpy(m_data, s, m_len);
        m_data[m_len] = '\0';
    }
    char* m_data;
    size_t m_len;
};

void test()
{
    vector vec;
    MyString a;// 没有输出
    a = MyString("hello");
    vec.push_back(MyString("world"));
}

int main()
{
    test();

    system("pause");
    return 0;
}

输出:
构造函数hello
移动等号操作符重载hello
构造函数world
移动拷贝构造函数world
析构函数
析构函数

需要注意的是:右值引用并不能阻止编译器在临时对象使用完之后将其释放掉的事实,
所以移动构造函数和移动赋值操作符重载函数 中都将_data赋值为了NULL,而且析构函数中保证了_data != NULL才会释放。

标准库函数std::move

  既然编译器只对右值引用才能调用移动构造函数和移动赋值函数,又因为所有命名对象都只能是左值引用。
  在这样的条件了,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数
  也就是把一个左值引用当做右值引用来使用,怎么做呢?
  标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

#include 
using namespace std;

void ProcessValue(int& i)
{
    cout << "LValue processed:" << i << endl;
}

void ProcessValue(int&& i)
{
    cout << "RValue processed:" << i << endl;
}

int main()
{
    int a = 1;
    ProcessValue(a);
    ProcessValue(move(a));

    system("pause");
    return 0;
}

输出:

LValue processed:1

RValue processed:1

std::move在提高swap函数性能上有非常大的帮助,一般来说,swap函数的通用定义如下:

template 
void swap(T& a, T& b)
{
    T tmp(a);// copy a to tmp
    a = b;// copy b to a
    b = tmp;//copy tmp to b
}

结合std::move 和 右值引用,可以避免不必要的拷贝。swap的定义变为:

#include 
#include 
using namespace std;

class MyString
{
public:
    MyString()
    {
        m_data = NULL;
        m_len = 0;
    }

    MyString(const char* s)
    {
        m_len = strlen(s);
        init_data(s);
        cout << "构造函数" << s << endl;
    }

    MyString(MyString&& str)
    {
        cout << "移动构造函数" << str.m_data << endl;
        m_len = str.m_len;
        m_data = str.m_data;
        str.m_len = 0;
        str.m_data = NULL;// 防止在析构函数中将内存释放掉       
    }

    MyString& operator=(MyString&& str)
    {
        cout << "移动等号操作符重载" << str.m_data << endl;
        if ( this != &str ){
            this->m_len = str.m_len;
            this->m_data = str.m_data;
            str.m_len = 0;
            str.m_data = NULL;// 防止在析构函数中将内存释放掉  
        }
        return *this;
    }

    ~MyString()
    {
        if ( m_data != NULL ){
            cout << "析构函数" << endl;
            free(m_data);
        }
    }

private:
    void init_data(const char* s)
    {
        m_data = new char[m_len + 1];
        memcpy(m_data, s, m_len);
        m_data[m_len] = '\0';
    }
    char* m_data;
    size_t m_len;
};

namespace MyT{
template 
void swap(T& a, T& b)
{
    T tmp(std::move(a));// move a to tmp
    a = std::move(b);// move b to a
    b = std::move(tmp);//move tmp to b
}
}

void test()
{
    MyString a("hello");
    MyString b("world");
    MyT::swap(a, b);
}

int main()
{
    test();

    system("pause");
    return 0;
}

精确传递(perfect Forwarding)

  精确传递就是在参数传递过程中,所有这些属性和参数值都不能改变。在泛型函数中,这样的需求非常普遍

  forward_value函数只有一个参数val,定义如下:

template  
void forward_value(const T& val) { 
    process_value(val); 
} 

template  
void forward_value(T& val) { 
    process_value(val); 
}

函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&,否则,下面四种不同类型参数的调用中就不能同时满足:

int a = 0; 
const int &b = 1; 
forward_value(a); // int& 
forward_value(b); // const int& 
forward_value(2); // int&

对于一个参数就要重载两次,也就是函数重载的次数和参数的个数是一个正比的关系。这个函数的定义次数对于程序员来说,是非常低效的。我们看看右值引用如何帮助我们解决这个问题:

template  
void forward_value(T&& val) { 
    process_value(val); 
}

只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。

你可能感兴趣的:(【C/C++】)