目录
-
- 左值、右值、左值引用、右值引用
- 右值引用和统一引用
- 使用右值引用,避免深拷贝,优化程序性能
- std::move()移动语义
- std::forward()完美转发
- 容器中的emplace_back()
C++11增加了一个新的类型,称作右值引用(R-value reference),标记为T&&,右值引用结合std::move可以很好的优化程序的效率。
1.左值、右值、左值引用、右值引用
左值是有名字的,对应了一定的内存区域,可访问;右值不具名,不对应内存域,不可访问,临时对像是右值。区分表达式的左右值属性有一个简便方法:若可对表达式用 & 符取址,则为左值,否则为右值。左值引用是对左值的引用,右值引用是对右值的引用。在C++11之前只存在左值引用,常量的引用是不合法的,在C++11中引入了右值引用。
1 const int& cra = 1; 2 int& ra = 1;//error 3 int && a = 1; //&&为右值引用
2.右值引用和统一引用
“T&&”有两个不同的含义。一个当然是右值引用,这个引用表现出你所期望的:它们仅仅绑定到右值,它们的主要差事就是识别出那些可以被移动的对象。“T&&”另一个含义是既是右值引用,又是左值引用,称为统一引用。这样的引用在代码中看上去像右值引用(也就是T&&),但它们可以表现的像是左值引用。它们的双重特性使之可以既绑定到右值(像右值引用一样),也可以绑定到左值(像左值引用)。统一引用出现在两种场景下,最常见的是函数模板的参数,第二个场景是auto声名,两个场景共同之处是有类型推导的出现。在模板f中,param的类型是推导的,对var2的声明中,var2的类型也是推导的,如果想更详细了解&&,可以参考scott-meyers这个文章:http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
1 void f(Widget&& param); // 右值引用 2 Widget&& var1 = Widget(); // 右值引用 3 template<typename T> 4 void f(std::vector<T>&& param); // 右值引用 5 template<typename T> 6 void f(T&& param); // 统一引用 7 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[1]) { *_data = ‘\0‘; std::cout << "default constructor" << std::endl; } MyString(const char* str) : _data(new char[strlen(str) + 1]) { std::cout << "constructor" << std::endl; strcpy(_data, str); } //copy constructor MyString(const MyString& rhs) : _data(new char[rhs.size() + 1]) { 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() + 1]; strcpy(this->_data, rhs._data); return *this; } bool operator==(const MyString& rhs) { if (strcmp(this->_data, rhs._data) == 0) { return true; } else { return false; } } char operator[](size_t index) { if (index >= strlen(_data)) { return ‘0‘; } else { return _data[index]; } } friend MyString operator+(const MyString& lhs, const MyString& rhs) { MyString str; str._data = new char[lhs.size() + rhs.size() + 1]; 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 0; }
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可以提高性能。
参考: