右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用好处?
右值引用是用来支持移动语义的。利用匿名的变量,让其交出所有权,避免复制数据,可以提高程序的效率,因此,如果一个临时变量再也用不着了,可以让其强制移动语义,这样,程序不用再进行大量的数据复制了。
移动语义?
即移动构造函数接受一个”右值引用”参数。
必须通过构造移动构造函数和移动赋值函数来实现移动和复制语义。
移动语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
为什么必须是右值引用?
首先为了不拷贝大对象,所以使用引用。
但是由于引用对象是右值(如字面值或者临时对象(临时对象表示没有名字的对象,如int(3.14)或3+4)),因为这类值本身就是用于临时存储,所以读取其值不影响程序正确性。
所以在这样的情况下我们如果需要一个单独的传入的参数也不需要拷贝了,就直接将这个传入的参数“据为己有”即可。
总结起来的话,移动构造函数就是实参—>形参,形参—>新创建的对象,这两个过程的拷贝都省了。
用法
-
消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
class MyString { private: char* m_data; size_t m_len; void copy_data(const char *s) { m_data = new char[m_len+1]; memcpy(_data, s, m_len); m_data[_len] = '\0'; } public: MyString() { m_data = NULL; m_len = 0; } MyString(const char* p) { m_len = strlen (p); copy_data(p); } MyString(const MyString& str) { //原拷贝构造函数 m_len = str.m_len; copy_data(str.m_data); std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl; } MyString& operator=(const MyString& str) { if (this != &str) { m_len = str.m_len; copy_data(str._data); } std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl; return *this; } //优化 MyString(MyString&& str) { // 移动构造函数 std::cout << "Move Constructor is called! source: " << str._data << std::endl; m_len = str.m_len;//右值移动本质是控制权的转交,将临时对象的内存控制权交给新对象 m_data = str.m_data; //避免了不必要的拷贝 str.m_len = 0; str.m_data = NULL; //注意!同时要移后源指针置为空,应保证不对源对象做其它操作 } MyString& operator=(MyString&& str) { // 移动赋值函数 =号重载 std::cout << "Move Assignment is called! source: " << str._data << std::endl; if (this != &str) { m_len = str.m_len; m_data = str.m_data; //避免了不必要的拷贝 str.m_len = 0; str.m_data = NULL; } return *this; } virtual ~MyString() { if (m_data) free(m_data); } }; void test() { MyString a; a = MyString("Hello"); std::vector<MyString> vec; vec.push_back(MyString("World")); }
-
完美转发/精确传递。(能够更简洁明确地定义泛型函数。)
就是在参数传递过程中,所有这些属性和参数值都不能/不会改变。在泛型函数中,这样的需求非常普遍。template <typename T> void f(T& val) { cout << val << endl; }//假设需要这样的函数,每一个参数必须重载两种类型,T& 和 const T&,否则,下面不同类型参数的调用中就不能同时满足。 int a = 0; const int &b = 1; f(a); // int& f(b); // const int& f(2); // int&& ... //所以用到右值引用 完美转发 template <typename T> void f(T&& val) { cout << val << endl; } //只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。 //四种不用类型参数的调用都能满足(参数的左右值属性和 const/non-cosnt 属性)完全传递。 int a = 0; const int &b = 1; f(a); // int& f(b); // const int& f(4+77); // int&&
注意:
int &&a=1;和const int &a=1;是完全一样的操作,先在数据区开辟一个值为1的无名整型量,再将引用a与这个整型量进行绑定。但是右值引用直接支持rebind(如将右值变成non-const左值)。
std::move()
编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用.
std::move语句可以将左值变为右值而避免拷贝构造。
如className v2(v1)其中v1是左值,会调用拷贝函数,如className v2(std::move(v1))可以调用转移函数。
std::forward<T>()
为转发而生,它会按照参数本来的类型转发出去,不管参数类型是T&&这种未定的引用类型还是明确的左值引用或者右值引用。
下面举例
template<typename T>
void PrintT(T& t) {
cout << "lvaue" << endl;
}
template<typename T>
void PrintT(T && t) {
cout << "rvalue" << endl;
}
template<typename T>
void TestForward(T && v) {
PrintT(v);
PrintT(std::forward<T>(v));
PrintT(std::move(v));
}
int main() {
TestForward(1);
int x = 1;
TestForward(x);
TestForward(std::forward<int>(x));
}
//输出
lvaue
rvalue
rvalue
lvaue
lvaue
rvalue
lvaue
rvalue//因为x连续两个调用都是完美转发,因此推导还是T&&
rvalue
关于引用折叠
由于引用本身不是一个对象,C++标准不允许直接定义引用的引用。如“int& & a = b;”这样的语句是编译不过的(注意两个&中间有空格,不是int&&)。
当类型推导时可能会间接地创建引用的引用,此时必须进行引用折叠。具体折叠规则如下:
- X& &、X& &&和X&& &都折叠成类型X&。即凡是有左值引用参与的情况下,最终的类型都会变成左值引用。
- 类型X&& &&折叠成X&&。即只有全部为右值引用的情况才会折叠为右值引用。
引用折叠规则暗示我们,可以将任意类型的实参传递给T&&类型的函数模板参数。
右值实参为右值引用,左值实参仍然为左值引用。
一句话,就是参数的属性不变。这样也就完美的实现了参数的完整传递。
应用
新版本的swap函数的标准实现就是三个move,而没有临时对象。所以非常高效
template <class T> void swap (T& a, T& b){
T c(std::move(a));
a=std::move(b);
b=std::move(c);
}
vector的push_back,新版本参数是右值引用,会根据传入的具体类型(左值构造,右值移动)来操作。