C++11中右值引用和移动语义

2023-06-10,,

目录

      左值、右值、左值引用、右值引用
      右值引用和统一引用
      使用右值引用,避免深拷贝,优化程序性能
      std::move()移动语义
      std::forward()完美转发
      容器中的emplace_back()

C++11增加了一个新的类型,称作右值引用(R-value reference),标记为T&&,右值引用结合std::move可以很好的优化程序的效率

1.左值、右值、左值引用、右值引用

  左值是有名字的,对应了一定的内存区域,可访问;右值不具名,不对应内存域,不可访问,临时对像是右值。区分表达式的左右值属性有一个简便方法:若可对表达式用 & 符取址,则为左值,否则为右值。左值引用是对左值的引用,右值引用是对右值的引用。在C++11之前只存在左值引用,常量的引用是不合法的,在C++11中引入了右值引用。

 const int& cra = ;
int& ra = ;//error
int && a = ; //&&为右值引用

2.右值引用和统一引用

“T&&”有两个不同的含义。一个当然是右值引用,这个引用表现出你所期望的:它们仅仅绑定到右值,它们的主要差事就是识别出那些可以被移动的对象。“T&&”另一个含义是既是右值引用,又是左值引用,称为统一引用。这样的引用在代码中看上去像右值引用(也就是T&&),但它们可以表现的像是左值引用。它们的双重特性使之可以既绑定到右值(像右值引用一样),也可以绑定到左值(像左值引用)。统一引用出现在两种场景下,最常见的是函数模板的参数,第二个场景是auto声名,两个场景共同之处是有类型推导的出现。在模板f中,param的类型是推导的,对var2的声明中,var2的类型也是推导的,如果想更详细了解&&,可以参考scott-meyers这个文章:http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers

 void f(Widget&& param);               // 右值引用
Widget&& var1 = Widget(); // 右值引用
template<typename T>
void f(std::vector<T>&& param); // 右值引用
template<typename T>
void f(T&& param); // 统一引用
auto&& var2 = var1; // 统一引用

3.右值引用,避免深拷贝

右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。消除了临时对象的维护 ( 创建和销毁 ) 对性能的影响。以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。

#include <iostream>
#include <cstring>
#include <vector>
using namespace std; class MyString {
private:
char* _data;
public:
//default constructor
MyString() :
_data(new char[]) {
*_data = '\0';
std::cout << "default constructor" << std::endl;
}
MyString(const char* str) :
_data(new char[strlen(str) + ]) {
std::cout << "constructor" << std::endl;
strcpy(_data, str);
} //copy constructor
MyString(const MyString& rhs) :
_data(new char[rhs.size() + ]) {
std::cout << "copy constructor" << endl;
strcpy(this->_data, rhs.c_str());
} MyString(MyString&& rhs){
std::cout << "move copy constructor" << std::endl;
this->_data = rhs._data;
rhs._data = nullptr;
} MyString& operator=(MyString&& rhs){
std::cout << "move assign " << std::endl;
if(this->_data != rhs._data){
this->_data = rhs._data;
rhs._data = nullptr;
}
return *this;
}
~MyString() {
delete[] _data;
} //assign
MyString& operator=(const MyString& rhs) {
std::cout << "assign" << std::endl;
if (this->_data == rhs._data) {
return *this;
}
delete[] this->_data;
this->_data = new char[rhs.size() + ];
strcpy(this->_data, rhs._data);
return *this;
} bool operator==(const MyString& rhs) {
if (strcmp(this->_data, rhs._data) == ) {
return true;
} else {
return false;
}
} char operator[](size_t index) {
if (index >= strlen(_data)) {
return '';
} else {
return _data[index];
}
} friend MyString operator+(const MyString& lhs, const MyString& rhs) {
MyString str;
str._data = new char[lhs.size() + rhs.size() + ];
strcpy(str._data, lhs._data);
strcat(str._data, rhs._data);
return str;
} friend ostream& operator<<(ostream &out, const MyString& str) {
out << str._data;
return out;
} size_t size() const {
return strlen(_data);
} const char* c_str() const {
return _data;
}
};
int main() { MyString str1("hello"); //constructor
MyString str2(str1); //copy constructor MyString str3 = MyString("abc"); //constructor
MyString str4 = std::move(MyString("aba")); //constructor,move copy constructor; str2 = std::move(str3); //move assign
str2 = MyString("abc"); //constructor,move assign vector<MyString> v;
v.push_back(MyString("world")); //constructor,move copy constructor; return ;
}

4.std::move()移动语义

std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。这种移动语义是很有用的,比如我们一个对象中有一些指针资源或者动态数组,在对象的赋值或者拷贝时就不需要拷贝这些资源了。在c++11之前我们的拷贝构造函数和赋值函数可能要这样定义:假设一个A对象内部有一个资源m_ptr;

A& A::operator=(const A& rhs)
{
// 销毁m_ptr指向的资源
// 复制rhs.m_ptr所指的资源,并使m_ptr指向它
}

  上面的过程是可行的,但是更有效率的办法是直接交换a和临时对象中的资源指针,然后让临时对象的析构函数去销毁a原来拥有的资源。换句话说,当赋值操作符的右边是右值的时候,我们希望赋值操作符被定义成下面这样:

A& A::operator=(const A&& rhs)
{
// 仅仅转移资源的所有者,将资源的拥有者改为被赋值者 }

 这就是所谓的move语义。再看一个例子,假设一个临时容器很大,赋值给另一个容器。

{
std::list< std::string > tokens;//省略初始化...
std::list< std::string > t = tokens;
}
std::list< std::string > tokens;
std::list< std::string > t = std::move(tokens);

5.std::forword()完美转发

右值引用类型是独立于值的,一个右值引用参数作为函数的形参,在函数内部再转发该参数的时候它已经变成一个左值了,并不是它原来的类型了。因此,我们需要一种方法能按照参数原来的类型转发到另一个函数,这种转发被称为完美转发。所谓完美转发(perfect forwarding),是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。C++11中提供了这样的一个函数std::forward,它是为转发而生的,它会按照参数本来的类型来转发出去,不管参数类型是T&&这种未定的引用类型还是明确的左值引用或者右值引用。

template<typename T>
void print(T& t) {
std::cout << "lvalue" << std::endl;
} template<typename T>
void print(T&& t) {
std::cout << "rvalue" << std::endl;
} template<typename T>
void testForward(T && t) {
print(t);
print(std::forward<T>(t));
print(std::move(t));
}

6. 成员的emplace_back

 c++11中大部分容器都加了一个emplace_back成员函数,vector中它的定义是这样的:

template< class... Args >
void emplace_back( Args&&... args );

 这里的Args&&是一个未定的引用类型,因此它可以接收左值引用和右值引用,它的内部也是调用了std::forward实现完美转发的。因此如果我们需要往容器中添加右值、临时变量时,用emplace_back可以提高性能。

参考:

条款24:区别开统一引用和右值引用
(原创)C++11改进我们的程序之右值引用
C++11 标准新特性: 右值引用与转移语义

C++11中右值引用和移动语义的相关教程结束。

《C++11中右值引用和移动语义.doc》

下载本文的Word格式文档,以方便收藏与打印。