本文將介紹帶你一步步的了解 c++11 中:
- 右值校焦、右值引用
- 移動構造函數
- && 解密
- move 移動語義
- forward 完美轉發(fā)
產生原由
class Object
{
public:
//無參構造函數
Object() : m_num(new int(10))
{
std::cout << "contr function..." << std::endl;
printf("m_num 地址:%p\n", m_num);
}
//拷貝構造函數
Object(const Object& o) : m_num(new int(*o.m_num))
{
std::cout << "copy contr function..." << std::endl;
}
private:
int* m_num;
};
我們知道,某個類中如果含有指向堆內存的(一般含有指針)成員變量栅贴,如果不編寫拷貝構造函數斟湃,那么編譯器將調用默認的拷貝構造函數,即只進行淺拷貝檐薯。即下圖中只拷貝了 a 指針,將會出現 a注暗、b指向同一塊內存坛缕,為了防止堆區(qū)地址雙重釋放,那么應該編寫 拷貝構造函數 防止淺拷貝發(fā)生問題捆昏。即在拷貝構造函數中重新分配一塊內存進行初始化赚楚。
問題:在 c++11 中引入了右值引用和移動構造函數又是為了什么呢?
下面的代碼中調用 getObj
函數初始化一個對象 oo1
, 分析其執(zhí)行過程:
1骗卜、getObj
函數中初始化一個臨時對象 temp
宠页, 調用構造函數;
2寇仓、將臨時對象賦值給 oo1
举户, 調用拷貝構造函數;
class Object
{
...
}
Object getObj() {
//1遍烦、初始化一個臨時對象 temp俭嘁, 調用構造函數;
Object temp;
return temp;
}
int main() {
//2服猪、將臨時對象賦值給 `oo1`供填, 調用拷貝構造函數;
Object oo1 = getObj();
return 0;
};
執(zhí)行結果:
contr function...
m_num 地址:00E7F6D8
copy contr function...
根據執(zhí)行結果罢猪,與預期一致近她。
問題: 如果在第一步中,在調用 getObj
時膳帕,創(chuàng)建的臨時對象 temp
在構造過程中如果要進行大量的初始化工作(特別耗時)粘捎,并且其用完后將被釋放; 第二步中备闲,將臨時對象拷貝給 oo1
時晌端,也需要進行大量的拷貝工作。oo1
在生命周期結束后也將釋放恬砂。
思考: 中間產生了臨時對象 temp
只起到賦值作用咧纠,極大的耗費性能。有什么方式可以避免產生這個中間的臨時變量呢泻骤?怎么去優(yōu)化它呢漆羔?
答案: c++ 11
的右值引用梧奢。
右 值
c++11 中引入了右值的概念,使用 && 標記演痒。
不必要去記其概念亲轨,只需要知道怎么去判別即可,可以被取地址的即為左值鸟顺,反之為右值惦蚊。
int x = 1000;
int y = 2000;
x = y;
其中, 等號左邊的 x讯嫂、y 可以取地址蹦锋,即為左值; 等號右邊 1000欧芽、 2000 為右值莉掂; 處于等號左右的 x = y 中,因為其都可以進行取地址千扔,所以都為左值憎妙。
右值引用
右值引用也即是一個引用,和左值引用一樣曲楚,只不過左值引用是左值的別名厘唾。右值不具備名字,所以只能使用右值引用標記它洞渤。因為左值引用和右值引用都是別名阅嘶,不擁有所綁定對象的內存,所以必須進行初始化操作载迄。右值被右值引用接收后重新有了名字讯柔,只要該引用變量存活,护昧,右值也將存活魂迄。即右值引用可以延長某塊內存的存活時間。
int&& data = 1000; //必須進行初始化
class Object
{
public:
Object()
{
std::cout << "contr function..." << std::endl;
}
Object(const Test& a)
{
std::cout << "copy contr function..." << std::endl;
}
};
Object getObj()
{
return Object();
}
int main()
{
int a1;
int &&a2 = a1; // error
Object& t = getObj(); // error
Object && t = getObj();
const Object& t = getObj();
return 0;
}
-
int &&a2 = a1;
a1 具有名字惋耙,其為左值捣炬,左值賦值給右值引用 錯誤 -
Object& t = getObj();
getObj 函數返回一個沒有名字的右值,將一個右值賦值給左值引用 錯誤 -
Object && t = getObj();
右值賦值給右值引用 正確 -
const Object& t = getObj();
常量左值引用被成為萬能引用绽榛,既可以引用左值也可以引用右值 正確
性能優(yōu)化
介紹完右值和右值引用湿酸, 在回到上面的問題:中間產生了臨時對象
temp只起到賦值作用,極大的耗費性能灭美。有什么方式可以避免產生這個中間的臨時變量呢推溃?怎么去優(yōu)化它呢?
class Object
{
...
}
Object getObj() {
//1届腐、初始化一個臨時對象 temp铁坎, 調用構造函數蜂奸;
Object temp;
return temp;
}
int main() {
//2、將臨時對象賦值給 `oo1`硬萍, 調用拷貝構造函數扩所;
Object oo1 = getObj();
return 0;
};
getObj
函數中創(chuàng)建的臨時對象(堆上)構建完成后,還沒有使用朴乖,就釋放掉了祖屏,那么如果可以復用這個臨時對象,將會對性能有很大幫助寒砖。
那么如何復用呢赐劣? 給該類編寫移動構造函數即可。
class Object
{
...
//移動構造函數
Object(Object&& o) {
m_num = o.m_num;
o.m_num = nullptr;
std::cout << "move contr function..." << std::endl;
}
private:
int* m_num;
}
Object getObj() {
//1哩都、初始化一個臨時對象 temp, 調用構造函數婉徘;
Object temp;
return temp;
}
int main() {
//2漠嵌、 調用移動構造函數
Object oo1 = getObj();
return 0;
};
執(zhí)行結果:
contr function...
m_num 地址:00CCF5B0
move contr function...
執(zhí)行結果調用了移動構造函數。那么我們分析移動構造函數中發(fā)生了什么盖呼?
//移動構造函數
Object(Object&& o) {
m_num = o.m_num;
o.m_num = nullptr;
std::cout << "move contr function..." << std::endl;
}
...
Object oo1 = getObj();
因為臨時對象用完就釋放儒鹿,白白構造那么長時間。在執(zhí)行 Object oo1 = getObj();
這條語句是几晤,調用移動構造函數將臨時對象 temp
所指的內存直接賦值給 oo1
對象的指針(m_num = o.m_num;
); 然后避免 temp 出了作用域銷毀內存约炎,則將 temp 指向的內存置空(o.m_num = nullptr;
)。 oo1
對象直接擁有了 構造 temp
時分配的內存蟹瘾。
上圖充分的展示了拷貝構造函數和移動構造函數的關系圾浅,可以看出,移動構造函數整體上少分配了一塊內存憾朴,相當于淺拷貝狸捕,只不過最后將原始指針置空,因此極大的節(jié)省了空間和時間众雷。
問題1: 什么時候會調用移動構造函數灸拍?
答案: 要求右側的對象是一個臨時對象,才會調用移動構造函數砾省,如果沒有移動構造函數鸡岗,則將調用拷貝構造函數。因此可以看出编兄,移動構造函數不是必須存在的轩性,只是為了性能優(yōu)化而存在的。
可以將上述代碼中移動構造函數注釋掉翻诉,編譯器將會調用拷貝構造函數炮姨。
問題2: 怎么編寫移動構造函數呢捌刮?
從上面可以看出,移動構造函數實質是為了復用其他對象的資源而產生的舒岸,這種資源往往是堆內存的資源绅作。那么在編寫移動構造函數過程中,只需要轉移該類中關于堆上的資源即可蛾派。
右值符號 && 解密
在很多代碼模板函數中經常會出現諸如以下的代碼:
1俄认、 static_cast<typename remove_reference<_Ty>::type&&>(_Arg)
2、 typename<class T> void function(T&& t)
代碼中的 && 會不會讓你暈頭轉向洪乍? 如果是眯杏,那么和我一起解密吧!
c++ 中有一種叫做未定義的引用類型壳澳,通常有以下兩種方式:
自動類型推導的 auto&&
模板類型推導的 T&&
有一種特列 const T&&
岂贩, 表示右值引用,不屬于未定義類型引用巷波。
那么接下來記住兩個規(guī)則即可萎津,不對,是一個規(guī)則(引用折疊):
使用右值推導 T&& 和 auto&& 得到的是一個右值引用類型抹镊;其他的都是左值引用類型锉屈。
int a = 10;
int b = 250;
auto&& x = a; //a 是一個左值, auto&& 表示左值引用
auto&& y = 100; //100 是右值垮耳, auto&& 表示右值引用
int&& a1 = 5;
auto&& b1 = a1; //a1是右值引用颈渊,不是右值,所以b1 是左值引用
int a2 = 10;
int& a3 = a2; //a2是左值终佛,a3為左值引用
auto&& c1 = a3; // a3是左值引用俊嗽, c1 即是左值引用類型
auto&& c2 = a2; // a2是左值, c2 即是左值引用類型
const int& d1 =3;
const int&& d2 = 4;
auto&& e1 = d1; //d1是常量左值引用查蓉, e1 即為常量左值引用
auto&& e2 = d2; //d2是常量右值引用乌询, e2 即為常量左值引用
通過以上既可以理解,使用右值推導 T&& 和 auto&& 得到的都是右值引用類型豌研,其他的都是左值引用類型妹田。
void printX(int &x)
{
std::cout << "l-value: " << x << std::endl;
}
void printX(int &&x)
{
cout << "r-value: " << x << endl;
}
void forward(int &&x)
{
printX(x);
}
int main()
{
int a = 100;
printX(a);
printX(20);
forward(500);
return 0;
system("pause");
};
上面定義了兩個重載函數 printX
, 先對上面的輸出結果進行推導:
1、 printX(a)
; 其中 a
是一個左值鹃共,那么調用第一個 printX
函數鬼佣,輸出應該是左值;
2霜浴、 printX(20)
; 其中 20 是右值晶衷, 那么調用第二個printX
函數, 輸出應該是右值;
3晌纫、 forward(500)
, 其中 500 是右值税迷,forward
形參x
是右值引用類型,繼續(xù)調用printX
锹漱,由于此時的右值具備的名字箭养,所以將退化成一個左值,所以將調用第一個 printX
函數哥牍, 輸出應該是左值毕泌;
對于最后以重情況可能稍微難以理解,只需要記住嗅辣,右值引用在傳遞的過程中將會退化成左值引用撼泛。這也是 std::forward
為了防止退化成左值引用,所以才出現的澡谭,被譽為完美轉發(fā) 愿题,即不做任何變動的轉發(fā)。將上述 forward
函數中的 printX(x)
改成 printX(std::forward<int>(x))
將會調用第二個函數蛙奖,輸出應該是右值抠忘。
輸出結果:
l-value: 100
r-value: 20
l-value: 500
結果完全正確。
std::move
在上述的例子中外永,有一種情況是不能進行賦值的,即用一個左值初始化一個右值引用拧咳;
int a = 10;
int&& b = a; //error 無法將右值引用綁定到左值
所以 std::move
函數應運而生伯顶, move
通常被理解為“移動”, 但在本文中被譯為“轉移”更加合適骆膝,即轉移所有權祭衩,將你的房產名字轉給你的老婆,房子本身不變阅签,只是所有者發(fā)生了變化掐暮。
再來看 std::move 的源代碼:
template<class _Ty> inline
_CONST_FUN typename remove_reference<_Ty>::type&&
move(_Ty&& _Arg) _NOEXCEPT
{ // forward _Arg as movable
return (static_cast<typename remove_reference<_Ty>::type&&>(_Arg));
}
使用 std::move 后,上述代碼可以更改為:
int a = 10;
int&& b = std::move(a); //ok
對于這種將左值轉化為右值的方法有什么用處呢政钟?
vector<string> vec;
vec.push_back("wang");
vec.push_back("zhuo");
.....
//插入一百萬條數據
.....
vector<string> vec1 = vec;
vector<string> vec2 = std::move(vec);
如果用 vec
這個左值直接初始化 vec1
路克,將會發(fā)生大量的內存拷貝。y
如果用 vec2 = std::move(vec),
直接將 vec
的所有權轉移給 vec2
即可
用處养交? 在對于擁有大量的堆內存或者動態(tài)數組時候精算,使用 std::move 可以有效的節(jié)省時間效率。
如果將 std::move 和 移動構造函數結合起來碎连,盡可能重復利用資源灰羽, 移動構造函數接收的是一個右值引用類型。
std::forward
上文也提及到了完美轉發(fā) forward,即在右值引用傳遞的過程中廉嚼,為了防止被編譯器當作左值處理玫镐,使其以原由類型進行轉發(fā),引入了 forward怠噪。
原型如下:
std::forward<T>(x);
當forward 的模板類型參數 T 為左值引用類型時恐似,x將被轉換成 T類型的左值,否則將被轉換成右值舰绘。
template<typename T>
void printX(T& t)
{
std::cout << "left value"<< std::endl;
}
template<typename T>
void printX(T&& t)
{
std::cout << "rifht value " <<std::endl;
}
template<typename T>
void test(T && v)
{
printX(v);
printX(move(v));
printX(forward<T>(v));
}
int main()
{
test(100);
int num = 10;
test(num);
test(forward<int>(num));
test(forward<int&>(num));
test(forward<int&&>(num));
return 0;
}
1蹂喻、test(100)
, 100 是右值,test
形參是未定義引用類型捂寿,即根據上文提到的 ”使用右值推導 T&&
和 auto&&
得到的是一個右值引用類型“ 形參 v 是一個具有名字的右值引用口四,但編譯器將其視為左值。
* 傳遞給第一個函數 `printX `變?yōu)樽笾登芈{用第一個蔓彩,輸出左值。
* 使用`move(v)` 之后驳概,左值 v 被 move 成右值赤嚼,輸出右值。
* `printX(forward<T>(v))`顺又, T為右值引用更卒,因此最終將會成為右值, 輸出右值
2稚照、test(forward<int&>(num))
; 模板參數為int&
蹂空, 根據 ”當forward
的模板類型參數 T 為左值引用類型時,x將被轉換成 T類型的左值“ 果录, 將會得到一個左值上枕,test
形參為未定義類型 T&&
, 根據 ”使用右值推導 T&&
和 auto&&
得到的是一個右值引用類型弱恒,反之為左值引用“辨萍,即test 形參為左值引用類型。
* `printX(v);` v 是左值返弹, 輸出左值
* `printX(move(v));` 左值經過 `move` 后成為右值锈玉,輸出右值
* `printX(forward<T>(v));` T類型為 `int&` ,v將被轉換成左值琉苇, 輸出左值嘲玫。
你學會了嗎? 其他的幾種留給你自己分析并扇。