c++中引入了右值引用
和移动语义
,能够避免无谓的复制,提升程序性能。有点难理解,因而花时间整理一下本身的理解。ios
C++
中全部的值都必然属于左值、右值两者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就再也不存在的临时对象。全部的具名变量或者对象都是左值,而右值不具名。很可贵到左值和右值的真正定义,可是有一个能够区分左值和右值的便捷方法:看能不能对表达式取地址,若是能,则为左值,不然为右值。c++
看见书上又将右值分为将亡值和纯右值。纯右值就是c++98
标准中右值的概念,如非引用返回的函数返回的临时变量值;一些运算表达式,如1+2产生的临时变量;不跟对象关联的字面量值,如2,'c',true,"hello";这些值都不可以被取地址。数组
而将亡值则是c++11
新增的和右值引用相关的表达式,这样的表达式一般时将要移动的对象、T&&
函数返回值、std::move()
函数的返回值等,安全
不懂将亡值和纯右值的区别其实不要紧,统一看做右值便可,不影响使用。ide
示例:函数
int i=0;// i是左值, 0是右值 class A { public: int a; }; A getTemp() { return A(); } A a = getTemp(); // a是左值 getTemp()的返回值是右值(临时变量)
c++98
中的引用很常见了,就是给变量取了个别名,在c++11
中,由于增长了右值引用(rvalue reference)的概念,因此c++98
中的引用都称为了左值引用(lvalue reference)。性能
int a = 10; int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用 int& b = 1; //编译错误! 1是右值,不可以使用左值引用
c++11
中的右值引用使用的符号是&&
,如学习
int&& a = 1; //实质上就是将不具名(匿名)变量取了个别名 int b = 1; int && c = b; //编译错误! 不能将一个左值复制给一个右值引用 class A { public: int a; }; A getTemp() { return A(); } A && a = getTemp(); //getTemp()的返回值是右值(临时变量)
getTemp()
返回的右值原本在表达式语句结束后,其生命也就该终结了(由于是临时变量),而经过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a
的生命期同样,只要a
还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。测试
注意:这里a
的类型是右值引用类型(int &&
),可是若是从左值和右值的角度区分它,它其实是个左值。由于能够对它取地址,并且它还有名字,是一个已经命名的右值。优化
因此,左值引用只能绑定左值,右值引用只能绑定右值,若是绑定的不对,编译就会失败。可是,常量左值引用倒是个奇葩,它能够算是一个“万能”的引用类型,它能够绑定很是量左值、常量左值、右值,并且在绑定右值的时候,常量左值引用还能够像右值引用同样将右值的生命期延长,缺点是,只能读不能改。
const int & a = 1; //常量左值引用绑定 右值, 不会报错 class A { public: int a; }; A getTemp() { return A(); } const A & a = getTemp(); //不会报错 而 A& a 会报错
事实上,不少状况下咱们用来常量左值引用的这个功能却没有意识到,以下面的例子:
#include <iostream> using namespace std; class Copyable { public: Copyable(){} Copyable(const Copyable &o) { cout << "Copied" << endl; } }; Copyable ReturnRvalue() { return Copyable(); //返回一个临时对象 } void AcceptVal(Copyable a) { } void AcceptRef(const Copyable& a) { } int main() { cout << "pass by value: " << endl; AcceptVal(ReturnRvalue()); // 应该调用两次拷贝构造函数 cout << "pass by reference: " << endl; AcceptRef(ReturnRvalue()); //应该只调用一次拷贝构造函数 }
当我敲完上面的例子并运行后,发现结果和我想象的彻底不同!指望中AcceptVal(ReturnRvalue())
须要调用两次拷贝构造函数,一次在ReturnRvalue()
函数中,构造好了Copyable
对象,返回的时候会调用拷贝构造函数生成一个临时对象,在调用AcceptVal()
时,又会将这个对象拷贝给函数的局部变量a
,一共调用了两次拷贝构造函数。而AcceptRef()
的不一样在于形参是常量左值引用,它可以接收一个右值,并且不须要拷贝。
而实际的结果是,无论哪一种方式,一次拷贝构造函数都没有调用!
这是因为编译器默认开启了返回值优化(RVO/NRVO, RVO, Return Value Optimization 返回值优化,或者NRVO, Named Return Value Optimization)。编译器很聪明,发如今ReturnRvalue
内部生成了一个对象,返回以后还须要生成一个临时对象调用拷贝构造函数,很麻烦,因此直接优化成了1个对象对象,避免拷贝,而这个临时变量又被赋值给了函数的形参,仍是不必,因此最后这三个变量都用一个变量替代了,不须要调用拷贝构造函数。
虽然各大厂家的编译器都已经都有了这个优化,可是这并非c++
标准规定的,并且不是全部的返回值都可以被优化,而这篇文章的主要讲的右值引用,移动语义能够解决编译器没法解决的问题。
为了更好的观察结果,能够在编译的时候加上-fno-elide-constructors
选项(关闭返回值优化)。
// g++ test.cpp -o test -fno-elide-constructors pass by value: Copied Copied //能够看到确实调用了两次拷贝构造函数 pass by reference: Copied
上面这个例子本意是想说明常量左值引用可以绑定一个右值,能够减小一次拷贝(使用很是量的左值引用会编译失败),可是顺便讲到了编译器的返回值优化。。编译器仍是干了不少事情的,颇有用,但不能过于依赖,由于你也不肯定它何时优化了何时没优化。
总结一下,其中T
是一个具体类型:
T&
, 只能绑定左值 T&&
, 只能绑定右值 const T&
, 既能够绑定左值又能够绑定右值 回顾一下如何用c++实现一个字符串类MyString
,MyString
内部管理一个C语言的char *
数组,这个时候通常都须要实现拷贝构造函数和拷贝赋值函数,由于默认的拷贝是浅拷贝,而指针这种资源不能共享,否则一个析构了,另外一个也就完蛋了。
具体代码以下:
#include <iostream> #include <cstring> #include <vector> using namespace std; class MyString { public: static size_t CCtor; //统计调用拷贝构造函数的次数 // static size_t CCtor; //统计调用拷贝构造函数的次数 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& operator=(const MyString& str){ 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() { delete[] m_data; } char* get_c_str() const { return m_data; } private: char* m_data; }; size_t MyString::CCtor = 0; int main() { vector<MyString> vecStr; vecStr.reserve(1000); //先分配好1000个空间,不这么作,调用的次数可能远大于1000 for(int i=0;i<1000;i++){ vecStr.push_back(MyString("hello")); } cout << MyString::CCtor << endl; }
代码看起来挺不错,却发现执行了1000
次拷贝构造函数,若是MyString("hello")
构造出来的字符串原本就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MyString("hello")
只是临时对象,拷贝完就没什么用了,这就形成了没有意义的资源申请和释放操做,若是可以直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间。而C++11
新增长的移动语义就可以作到这一点。
要实现移动语义就必须增长两个函数:移动构造函数和移动赋值构造函数。
#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 */
能够看到,移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const MyString& str
,是常量左值引用,而移动构造的参数是MyString&& str
,是右值引用,而MyString("hello")
是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不一样,它并非从新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将本身的指针指向别人的资源,而后将别人的指针修改成nullptr
,这一步很重要,若是不将别人的指针修改成空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。下面这张图能够解释copy和move的区别。
不用奇怪为何能够抢别人的资源,临时对象的资源很差好利用也是浪费,由于生命周期原本就是很短,在你执行完这个表达式以后,它就毁灭了,充分利用资源,才能很高效。
对于一个左值,确定是调用拷贝构造函数了,可是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11
为了解决这个问题,提供了std::move()
方法来将左值转换为右值,从而方便应用移动语义。我以为它其实就是告诉编译器,虽然我是一个左值,可是不要对我用拷贝构造函数,而是用移动构造函数吧。。。
int main() { vector<MyString> vecStr; vecStr.reserve(1000); //先分配好1000个空间 for(int i=0;i<1000;i++){ MyString tmp("hello"); vecStr.push_back(tmp); //调用的是拷贝构造函数 } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; cout << endl; MyString::CCtor = 0; MyString::MCtor = 0; MyString::CAsgn = 0; MyString::MAsgn = 0; vector<MyString> vecStr2; vecStr2.reserve(1000); //先分配好1000个空间 for(int i=0;i<1000;i++){ MyString tmp("hello"); vecStr2.push_back(std::move(tmp)); //调用的是移动构造函数 } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; } /* 运行结果 CCtor = 1000 MCtor = 0 CAsgn = 0 MAsgn = 0 CCtor = 0 MCtor = 1000 CAsgn = 0 MAsgn = 0 */
下面再举几个例子:
MyString str1("hello"); //调用构造函数 MyString str2("world"); //调用构造函数 MyString str3(str1); //调用拷贝构造函数 MyString str4(std::move(str1)); // 调用移动构造函数、 // cout << str1.get_c_str() << endl; // 此时str1的内部指针已经失效了!不要使用 //注意:虽然str1中的m_dat已经称为了空,可是str1这个对象还活着,知道出了它的做用域才会析构!而不是move完了马上析构 MyString str5; str5 = str2; //调用拷贝赋值函数 MyString str6; str6 = std::move(str2); // str2的内容也失效了,不要再使用
须要注意一下几点:
str6 = std::move(str2)
,虽然将str2
的资源给了str6
,可是str2
并无马上析构,只有在str2
离开了本身的做用域的时候才会析构,因此,若是继续使用str2
的m_data
变量,可能会发生意想不到的错误。std::move()
会失效可是不会发生错误,由于编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&
常量左值引用的缘由!c++11中
的全部容器都实现了move
语义,move
只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move
对于拥有如内存、文件句柄等资源的成员的对象有效,若是是一些基本类型,如int和char[10]数组等,若是使用move,仍会发生拷贝(由于没有对应的移动构造函数),因此说move
对含有资源的对象说更有意义。当右值引用和模板结合的时候,就复杂了。T&&
并不必定表示右值引用,它多是个左值引用又多是个右值引用。例如:
template<typename T> void f( T&& param){ } f(10); //10是右值 int x = 10; // f(x); //x是左值
若是上面的函数模板表示的是右值引用的话,确定是不能传递左值的,可是事实倒是能够。这里的&&
是一个未定义的引用类型,称为universal references
,它必须被初始化,它是左值引用仍是右值引用却决于它的初始化,若是它被一个左值初始化,它就是一个左值引用;若是被一个右值初始化,它就是一个右值引用。
注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&
才是一个universal references
。
例如:
template<typename T> void f( T&& param); //这里T的类型须要推导,因此&&是一个 universal references template<typename T> class Test { Test(Test&& rhs); //Test是一个特定的类型,不须要类型推导,因此&&表示右值引用 }; void f(Test&& param); //右值引用 //复杂一点 template<typename T> void f(std::vector<T>&& param); //在调用这个函数以前,这个vector<T>中的推断类型 //已经肯定了,因此调用f函数的时候没有类型推断了,因此是 右值引用 template<typename T> void f(const T&& param); //右值引用 // universal references仅仅发生在 T&& 下面,任何一点附加条件都会使之失效
因此最终仍是要看T
被推导成什么类型,若是T
被推导成了string
,那么T&&
就是string&&
,是个右值引用,若是T
被推导为string&
,就会发生相似string& &&
的状况,对于这种状况,c++11
增长了引用折叠的规则,总结以下:
如上面的T& &&
其实就被折叠成了个string &
,是一个左值引用。
#include <iostream> #include <type_traits> #include <string> using namespace std; template<typename T> void f(T&& param){ if (std::is_same<string, T>::value) std::cout << "string" << std::endl; else if (std::is_same<string&, T>::value) std::cout << "string&" << std::endl; else if (std::is_same<string&&, T>::value) std::cout << "string&&" << std::endl; else if (std::is_same<int, T>::value) std::cout << "int" << std::endl; else if (std::is_same<int&, T>::value) std::cout << "int&" << std::endl; else if (std::is_same<int&&, T>::value) std::cout << "int&&" << std::endl; else std::cout << "unkown" << std::endl; } int main() { int x = 1; f(1); // 参数是右值 T推导成了int, 因此是int&& param, 右值引用 f(x); // 参数是左值 T推导成了int&, 因此是int&&& param, 折叠成 int&,左值引用 int && a = 2; f(a); //虽然a是右值引用,但它仍是一个左值, T推导成了int& string str = "hello"; f(str); //参数是左值 T推导成了string& f(string("hello")); //参数是右值, T推导成了string f(std::move(str));//参数是右值, T推导成了string }
因此,概括一下, 传递左值进去,就是左值引用,传递右值进去,就是右值引用。如它的名字,这种类型确实很"通用",下面要讲的完美转发,就利用了这个特性。
所谓转发,就是经过一个函数将参数继续转交给另外一个函数进行处理,原参数多是右值,多是左值,若是还能继续保持参数的原有特征,那么它就是完美的。
void process(int& i){ cout << "process(int&):" << i << endl; } void process(int&& i){ cout << "process(int&&):" << i << endl; } void myforward(int&& i){ cout << "myforward(int&&):" << i << endl; process(i); } int main() { int a = 0; process(a); //a被视为左值 process(int&):0 process(1); //1被视为右值 process(int&&):1 process(move(a)); //强制将a由左值改成右值 process(int&&):0 myforward(2); //右值通过forward函数转交给process函数,却称为了一个左值, //缘由是该右值有了名字 因此是 process(int&):2 myforward(move(a)); // 同上,在转发的时候右值变成了左值 process(int&):0 // forward(a) // 错误用法,右值引用不接受左值 }
上面的例子就是不完美转发,而c++中提供了一个std::forward()
模板函数解决这个问题。将上面的myforward()
函数简单改写一下:
void myforward(int&& i){ cout << "myforward(int&&):" << i << endl; process(std::forward<int>(i)); } myforward(2); // process(int&&):2
上面修改事后仍是不完美转发,myforward()
函数可以将右值转发过去,可是并不可以转发左值,解决办法就是借助universal references
通用引用类型和std::forward()
模板函数共同实现完美转发。例子以下:
#include <iostream> #include <cstring> #include <vector> using namespace std; void RunCode(int &&m) { cout << "rvalue ref" << endl; } void RunCode(int &m) { cout << "lvalue ref" << endl; } void RunCode(const int &&m) { cout << "const rvalue ref" << endl; } void RunCode(const int &m) { cout << "const lvalue ref" << endl; } // 这里利用了universal references,若是写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值 template<typename T> void perfectForward(T && t) { RunCode(forward<T> (t)); } template<typename T> void notPerfectForward(T && t) { RunCode(t); } int main() { int a = 0; int b = 0; const int c = 0; const int d = 0; notPerfectForward(a); // lvalue ref notPerfectForward(move(b)); // lvalue ref notPerfectForward(c); // const lvalue ref notPerfectForward(move(d)); // const lvalue ref cout << endl; perfectForward(a); // lvalue ref perfectForward(move(b)); // rvalue ref perfectForward(c); // const lvalue ref perfectForward(move(d)); // const rvalue ref }
上面的代码测试结果代表,在universal references
和std::forward
的合做下,可以完美的转发这4种类型。
咱们以前使用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是可移动的,那么整个操做会很高效,若是不可移动,那么就和普通的交换函数是同样的,不会发生什么错误,很安全。
T&&
为模板参数时,输入左值,它将变成左值引用,输入右值则变成具名的右值应用。std::move()
将一个左值转换成一个右值,强制使用移动拷贝和赋值函数,这个函数自己并无对这个左值什么特殊操做。std::forward()
和universal references
通用引用共同实现完美转发。empalce_back()
替换push_back()
增长性能。std::move()和std::forward()
好像实现的并不复杂,有机会弄明白实现原理。