一.右值引用
C++11增加了一個新的類型,稱爲右值引用(R-value reference),標記爲T &&。右值是指表達式結束後就不再存在的臨時對象。相對應的左值就是指表達式結束後依然存在的持久對象,所有的具名變量或對象都是左值,而右值不具名。一個區分左值與右值的便捷方法是:看能不能對表達式取地址,如果能,則爲左值。
在C++11中,右值由兩個概念構成,一個是將亡值(xvalue,expiring value)(C++11新增的,與右值引用相關的表達式,比如將要被移動的對象,T&& 函數返回值,std::move返回值和轉換爲T&&的類型的轉換函數的返回值),另一個是純右值(rvalue, PureRvalue)(非引用返回的臨時變量,運算表達式產生的臨時言火日王,原始字面量和lambda表達式等都是純右值。C++11中所有的值必屬於左值,將亡值,純右值三者之一。
1.&&的特性
與左值引用相類似,右值引用就是對右值進行引用的類型。因爲右值不具名,所以,我們只能通過引用的方式找到它。
1).聲明右值引用時必須立即進行初始化。因爲引用類型並不擁有所綁定對象的別名,只是該對象的一個別名。
2).通過右值引用的聲明,該右值的生命週期將會與右值引用類型變量的生命週期一樣,只要變量還在,右值就會一直存活。
#include <iostream>
using namespace std;
int g_constructCount = 0;
int g_copyConstructCount = 0;
int g_destructCount = 0;
struct A
{
A(){
cout<<"construct: "<<++g_constructCount<<endl;
}
A(const A& a){
cout<<"copy construct: "<<++g_copyConstructCount<<endl;
}
~A(){
cout<<"destruct: "<<++g_destructCount<<endl;
}
};
A GetA(){
return A();
}
int main(){
A a = GetA();
return 0;
}
在關閉返回值優化的情況下,輸出結果:
construct: 1
copy construct: 1
destruct: 1
copy construct: 2
destruct: 2
destruct: 3
拷貝構造函數調用了兩次:
1).GetA()函數內部創建的對象返回後構造一個臨時對象時調用。
2).在main函數中構造a對象時調用。
優化(右值引用綁定了右值,讓臨時右值的生命週期延長了):
int main(){
A&& a = GetA();
return 0;
}
//輸出結果:
construct: 1
copy construct: 1
destruct: 1
destruct: 2
在C++98/03中,通過常量左值引用也經常用來做性能優化,輸出結果與右值引用一樣。因爲常量左值引用是一個“萬能”的引用類型,可以接受左值,右值,常量左值和常量右值。實際上,T&&並不是一定表示右值,它綁定的類型是未定的,即可能是左值又可能是右值。
template <typename T>
void f(T&& param);
f(10); //param是右值
int x = 10;
f(x); //param是左值
template<typename T>
void f(T&& param); //universal references
template<typename T>
class Test{
...
Test(Test&& hrs); //右值引用
...
};
void f(Test&& param); //右值引用
template <typename T>
void f (std::vector<T>&& param); //右值引用
temple <typename T>
void f(const T&& param); //右值引用
記住,如果不是universal references,用一個左值初始化一個右值引用類型是不合法的。
正確的做法是使用std::move將一個左值轉換成右值。
int w1;
decltype(w1)&& v1 = w1; //error
decltype(w1)&& v1 = std::move(w2);
編譯器會將己命名的右值引用視爲左值,而將未命名的右值引用視爲右值。
void PrintValue(int& i){
std::cout<<"lvalue : "<<i<<std::endl;
}
void PrintValue(int&& i){
std::cout<<"rvalue : "<<i<<std::endl;
}
void Forward(int&& i){
PrintValue(i);
}
int main(){
int i=0;
PrintValue(i);
PrintValue(1);
Forward(2);
}
輸出結果:
lvalue : 0
rvalue : 1
lvalue : 2
2.右值引用優化性能,避免深拷貝(C++11加入右值引用的原因)
對於含有堆內存的類,我們都需要提供其深拷貝的構造函數,否則,會使用其默認提供的拷貝構造函數,容易導致堆內存的重複刪除,指針指向爲空。
class A
{
public:
A():m_ptr(new int(0)){}
~A(){
delete m_ptr;
}
private:
int* m_ptr;
};
A get(bool flag){
A a;
A b;
if(flag){
return a;
}else{
return b;
}
}
int main(){
A a = Get(false); //臨時變量的m_ptr指向爲空,析構時,重複刪除引起錯誤...
}
下面是正確的做法,提供了深拷貝的拷貝構造函數
class A
{
public:
A() : m_ptr(new int(0)){
cout<<"constructor"<<endl;
}
A(const A& a):m_ptr(new int(*a.m_ptr)){
cout<<"copy construct"<<endl;
}
~A(){
cout<<"destruct"<<endl;
delete m_ptr;
}
private:
int* m_ptr;
};
int main(){
A a = Get(false);
}
輸出結果:
construct
construct
copy construct
destruct
destruct
destruct
這樣雖然是安全的,但是卻因爲拷貝構造帶來了額外的損耗。
Get函數會返回臨時變量,然後通過臨時變量拷貝構造一個新的對象b,臨時變量在拷貝構造完成之後銷燬了,如果堆內存很大,那麼這個拷貝構造的代價會很大。因爲可以使用移動構造函數(對右值引用進行淺拷貝)。
class A
{
public:
A():m_ptr(new int(0)){
cout<<"construct"<<endl;
}
A(const A& a):m_ptr(new int(*a.m_ptr)){
cout<<"copy construct"<<endl;
}
A(A&& a) : m_ptr(a.m_ptr){
a.m_ptr = nullptr;
cout<<"move construct: "<<endl;
}
~A(){
cout<<"destruct"<<endl;
delete m_ptr;
}
private:
int* m_ptr;
};
int main(){
A a = Get(false);
}
//輸出結果
construct
construct
move construct
destruct
destruct
destruct
移動構造函數中,它的參數是一個右值引用類型的參數A&&,這裏沒有深拷貝,只有淺拷貝,這樣就避免了對臨時對象的深拷貝,提高了性能。這裏的A&&用來根據參數是左值還是右值來建立分支,如果是臨時值,則會選擇移動構造函數。右值引用的一個重要的目的是用來支持移動語義的。
下面看一個MyString類實現的例子
class MyString{
private:
char * m_data;
size_t m_len;
void copy_data(const char *s){
m_data = new char[m_len+1];
memcpy(m_data, s, m_len);
m_data[m_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;
}
virtual ~MyString(){
if(m_data)
delete[] m_data;
}
};
int main(){
MyString a;
a = MyString("Hello");
std::vector<MyString> vec;
vec.push_back(MyString("World"));
return 0;
}
//MyString的移動構造函數和移動賦值函數
MyString(MyString&& str){
std::cout<<"Move Constructor is called! source: "<<str._data<<std::endl;
_len=str._len;
_data=str._data;
str._len=0;
str._data=NULL;
}
MyString& operator=(MyString&& str){
std::cout<<"Move Assignment is called! source: "<<str._data<<std::endl;
if(this!=&str){
_len=str._len;
_data=str._data;
str._len=0;
str._data=NULL;
}
return *this;
}
3.move語義
移動語義是通過右值引用來匹配臨時值的,普通的左值該怎麼辦呢?C++11提供了std::move方法來將左值轉換爲右值,從而方便應用移動語義。move是將對象的內存或者所有權從一個對象轉移到另一個對象,只是轉移,沒有內存拷貝,將一個左值強制轉換爲一個右值引用。
std::list<std::string> tokens;
std::list<std::string> t = std::move(tokens);
使用了move幾乎沒有任何代價,只是轉換了資源的所有權。實際上是將左值變成右值引用,然後應用move語義調用構造函數,就避免了拷貝,提高了性能。
4.forward和完美轉發
一個右值引用參數作爲函數的形參,在函數內部再轉發該參數時變成了一個左值,不是原來的類型了。
template <typename T>
void forwardValue(T& val){
processValue(val); //右值參數會變成左值
}
template <typename T>
void forwardValue(const T& val){
processValue(val); //參數都變成常量左值引用了
}
因此,我們需要一種方法能按照參數原來的類型轉發到另一個函數,這種轉發稱爲完美轉發。C++11提供了一個函數std::forward,爲轉發而生,不管參數是T&&這種未定的引用還是明確的左值引用或者右值引用,都會按照參數本來的類型轉發。
void Print(int& t){
cout<<"lvalue"<<endl;
}
template <typename T>
void PrintT(int &t){
cout<<"rvalue"<<endl;
}
template <typename T>
void TestForward(t && v){
PrintT(v);
PrintT(std::forward<T>(v));
PrintT(std::move(v));
}
Test(){
TestForward(1);
int x=1;
TestForward(x);
TestForward(std::forward<int>(x));
}
//輸出結果:
lvalue
rvalue
rvalue
5.emplace_back減少內存拷貝和移動
emplace_back能就地通過參數構造對象,不需要拷貝或者移動內存,相比push_back能更好地避免內存的拷貝和移動,使容器查入元素的性能得到進一步提升。在大多數情況下應該優先使用emplace_back來代替push_back。所有的標準庫容器(array除外,因爲它長度不可以變,不能插入元素)都增加了類似的方法:emplace,emplace_hint,emplace_front,emplace_after和emplace_back。
#include <vector>
#include <iostream>
using namespace std;
struct A
{
int x;
double y;
A(int a, double b):x(a),y(b){}
};
int main(){
vector<A> v;
v.emplace_back(1,2);
cout<<v.size()<<endl;
return 0;
}
emplace_back的用法比較簡單,直接通過構造函數的參數就可以構造對象,因此,也要求對象必須有對應的構造函數。如果沒有,編譯器會報錯。
#include <vector>
#include <map>
#include <string>
#include <iostream>
using namespace std;
struct Complicated
{
int year;
double country;
std::string name;
Complicated(int a, double b, string c):year(a),country(b),name(c){
cout<<"is constucted"<<endl;
}
Complicated(const Complicated& other):year(other.year),country(other.country),name(std::move(other.name)){
cout<<"is moved"<<endl;
}
};
int main(){
std::map<int, Complicated> m;
int anInt = 4;
double aDouble = 5.0;
std::string aString = "C++"
cout<<"--insert--"<<endl;
m.insert(std::make_pair(4,Complicated(anInt, aDouble, aString)));
cout<<"--emplace--"<<endl;
m.emplace(4, Complicated(anInt, aDouble, aString));
cout<<"--emplace_back--"<<endl;
vector<Complicated> v;
v.emplace_back(anInt, aDouble, aString);
cout<<"--push_back--"<<endl;
v.push_back(Complicated(anInt, aDouble, aString));
return 0;
}
//輸出結果:
--insert--
is constructed
is moved
is moved
--emplace--
is constructed
is moved
--emplace_back--
is constructed
--push_back--
is constructed
is moved
is moved