C++11中的新特性右值引用

一、 新特性的目的

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

二、左值和右值的概念

       C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。

  1. 左值是可以放在赋值号左边可以被赋值的对象;左值必须要在内存中有实体;通俗的讲左值的定义就是非临时对象,可以被取地址的对象,那些可以在多条语句中使用的对象。 所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。 
  2. 右值只能出现在赋值号右边取出值赋给其他变量;右值可以在内存也可以在CPU寄存器。 右值是指临时的对象,不可以被取地址,它们只在当前的语句中有效。
int i = 0;  // 在这条语句中,i 是左值,0 是临时值,就是右值。

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

int &a = 1;   // error C2440: “初始化”: 无法从“int”转换为“int &”

我们最多只能用常量引用来绑定一个右值,如:

const int &a = 1;

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

int &&a = 1;

三、 应用场景

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

class MyString {
private:
    char* _data;
    size_t   _len;
    void _init_data(const char *s) {
        _data = new char[_len + 1];
        memcpy(_data, s, _len);
        _data[_len] = '\0';
    }
public:
    MyString() {
        _data = NULL;
        _len = 0;
    }

    MyString(const char* p) {
        _len = strlen(p);
        _init_data(p);
    }

    MyString(const MyString& str) {
        _len = str._len;
        _init_data(str._data);
        std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
    }

    MyString& operator=(const MyString& str) {
        if (this != &str) {
            _len = str._len;
            _init_data(str._data);
        }
        std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
        return *this;
    }

    virtual ~MyString() {
        if (_data != NULL) {
            std::cout << "Destructor is called! " << std::endl; 
            free(_data);
        }
    }
};

int main() { 
    MyString a; 
    a = MyString("Hello"); 
    std::vector<MyString> vec; 
    vec.push_back(MyString("World")); 
}

运行结果:

Copy Assignment is called! source: Hello
Destructor is called!
Copy Constructor is called! source: World
Destructor is called!
Destructor is called!
Destructor is called!

总共执行了2次拷贝,MyString("Hello")MyString("World")都是临时对象,临时对象被使用完之后会被立即析构,在析构函数中free掉申请的内存资源。

如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。 这正是定义转移语义的目的。

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

    MyString(MyString&& str) { 
        std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
        _len = str._len; 
        _data = str._data; 
        str._len = 0; 
        str._data = NULL;   // ! 防止在析构函数中将内存释放掉
    }

    MyString& operator=(MyString&& str) { 
        std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
        if (this != &str) { 
            _len = str._len; 
            _data = str._data; 
            str._len = 0; 
            str._data = NULL;  // ! 防止在析构函数中将内存释放掉
        } 
        return *this; 
    }

运行结果:

Move Assignment is called! source: Hello
Move Constructor is called! source: World
Destructor is called!
Destructor is called!

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

四、 标准库函数 std::move

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

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

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

int main() { 
    int a = 0; 
    ProcessValue(a); 
    ProcessValue(std::move(a)); 
}

运行结果:

LValue processed: 0 
RValue processed: 0

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

template <class T> 
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函数的定义变为 :

template <class T>
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 
}

可以使用第三节中的MyString类进行测试:

int main() { 
    MyString a("a");
    MyString b("b");

    swap(a, b);

    return 0;
}

五、 精确传递(Perfect Forwarding)

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

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

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

template <typename T> 
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 <typename T> 
void forward_value(T&& val) { 
    process_value(val); 
}

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

 

 

转载:https://blog.csdn.net/china_jeffery/article/details/78520237

 

posted @ 2019-09-16 21:23  DH_HUSTer  阅读(18)  评论(0编辑  收藏  举报