右值引用和移动语义
C++中所有的值都必然属于左值、右值二者之一。
左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。
很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
1.左值引用, 使用 T&, 只能绑定左值
2.右值引用, 使用 T&&, 只能绑定右值
3.常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
4.已命名的右值引用,编译器会认为是个左值
5.编译器有返回值优化,但不要过于依赖
-
为什么要有移动语义?
int main() { vector<MyString> vecStr; vecStr.reserve(1000); //先分配好1000个空间,不这么做,调用的次数可能远大于1000,因为vector是2倍增长的 for(int i=0;i<1000;i++){ vecStr.push_back(MyString("hello")); } }
我们可以看到上面得代码中,不仅调用了1000次构造函数,还调用了1000次的赋值构造函数。这样会是多余的,造成了内存浪费,如果可以直接右值存进vector里面,就会加快内存申请和释放的时间。move语句就能实现将右值转为左值。
要实现移动语义要增加两个函数:移动语义就是将右值当作左值用。
#include <iostream> #include <cstring> #include <vector> using namespace std; class MyString { public: static size_t CCtor; //统计调用拷贝构造函数的次数 static size_t MCtor; //统计调用移动构造函数的次数 static size_t CAsgn; //统计调用拷贝赋值函数的次数 static size_t MAsgn; //统计调用移动赋值函数的次数 public: // 构造函数 MyString(const char* cstr=0){ if (cstr) { m_data = new char[strlen(cstr)+1]; strcpy(m_data, cstr); } else { m_data = new char[1]; *m_data = '\0'; } } // 拷贝构造函数 MyString(const MyString& str) { CCtor ++; m_data = new char[ strlen(str.m_data) + 1 ]; strcpy(m_data, str.m_data); } // 移动构造函数 MyString(MyString&& str) noexcept :m_data(str.m_data) { MCtor ++; str.m_data = nullptr; //不再指向之前的资源了 } // 拷贝赋值函数 =号重载 MyString& operator=(const MyString& str){ CAsgn ++; if (this == &str) // 避免自我赋值!! return *this; delete[] m_data; m_data = new char[ strlen(str.m_data) + 1 ]; strcpy(m_data, str.m_data); return *this; } // 移动赋值函数 =号重载 MyString& operator=(MyString&& str) noexcept{ MAsgn ++; if (this == &str) // 避免自我赋值!! return *this; delete[] m_data; m_data = str.m_data; str.m_data = nullptr; //不再指向之前的资源了 return *this; } ~MyString() { delete[] m_data; } char* get_c_str() const { return m_data; } private: char* m_data; }; size_t MyString::CCtor = 0; size_t MyString::MCtor = 0; size_t MyString::CAsgn = 0; size_t MyString::MAsgn = 0; int main() { vector<MyString> vecStr; vecStr.reserve(1000); //先分配好1000个空间 for(int i=0;i<1000;i++){ vecStr.push_back(MyString("hello")); } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; } /* 结果 CCtor = 0 MCtor = 1000 CAsgn = 0 MAsgn = 0 */
-
emplace_back减少内存拷贝和移动
我们之前使用vector一般都喜欢用push_back(),由上文可知容易发生无谓的拷贝,解决办法是为自己的类增加移动拷贝和赋值函数,但其实还有更简单的办法!就是使用emplace_back()替换push_back(),如下面的例子:
#include <iostream> #include <cstring> #include <vector> using namespace std; class A { public: A(int i){ // cout << "A()" << endl; str = to_string(i); } ~A(){} A(const A& other): str(other.str){ cout << "A&" << endl; } public: string str; }; int main() { vector<A> vec; vec.reserve(10); for(int i=0;i<10;i++){ vec.push_back(A(i)); //调用了10次拷贝构造函数 //vec.emplace_back(i); //一次拷贝构造函数都没有调用过 } for(int i=0;i<10;i++) cout << vec[i].str << endl; }
可以看到效果是明显的,虽然没有测试时间,但是确实可以减少拷贝。emplace_back()可以直接通过构造函数的参数构造对象,但前提是要有对应的构造函数。
对于map和set,可以使用emplace()。基本上emplace_back()对应push_bakc(), emplce()对应insert()。
移动语义对swap()函数的影响也很大,之前实现swap可能需要三次内存拷贝,而有了移动语义后,就可以实现高性能的交换函数了。template <typename T> void swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); }
如果T是可移动的,那么整个操作会很高效,如果不可移动,那么就和普通的交换函数是一样的,不会发生什么错误,很安全。