這篇博客有一些瑕疵的地方,原博客:http://avdancedu.com/a39d51f9/
移動構造函數是C++11中新增加的一種構造函數唱逢,其作用是提高程序性能验毡。今天我們就細扒一下它的工作原理逃延,看看它是怎么提高性能的典蜕。
移動構造函數的由來
在講解移動構造函數之間贺氓,我們先來了解一下在沒有移動構造函數之前哪里有性能瓶頸吧蹲缠。我們來舉個例子:
#include <iostream>
#include <vector>
#include <string.h>
class A {
public:
A(){
std::cout << "A construct..." << std::endl;
ptr_ = new int(100);
}
A(const A & a){
std::cout << "A copy construct ..." << std::endl;
ptr_ = new int();
memcpy(ptr_, a.ptr_, sizeof(int));
}
~A(){
std::cout << "A deconstruct ..." << std::endl;
if(ptr_){
delete ptr_;
}
}
A& operator=(const A & a) {
std::cout << " A operator= ...." << std::endl;
return *this;
}
int * getVal(){
return ptr_;
}
private:
int *ptr_;
};
int main(int argc, char *argv[]){
std::vector<A> vec;
vec.push_back(A());
}
//clang++ -g -o testmove test_move.cpp
上面這段代碼很簡單對吧仗扬,就是定義了一個普通的類A斋射。在main函數中創(chuàng)建一個vector育勺,然后用A類創(chuàng)建一個對象,并把它放入到vector中罗岖。這樣的程序在C++中是很常見怀大,但就是這樣很常見的代碼確有非常大的性能問題。為什么呢呀闻?因為在將A對象放入vector時化借,在vector內部又創(chuàng)建了一個A對象,并調用了其拷貝構造函數進行了深拷貝捡多。
我們看一下上面代碼運行的結果就一目了然了蓖康,其結果如下:
A construct... //main中創(chuàng)建的A對象
A copy construct ... //vector內部創(chuàng)建的A對象
A deconstruct ... //vector內部創(chuàng)建的A對象被析構
A deconstruct ... //main中創(chuàng)建的A對象析構
上面的運行結果印證了我們之前的討論铐炫,在vector內部確實又創(chuàng)建了一個A對象。如果在A對象中分配的是一個比較大的空間蒜焊,且vector中要存放大量的A對象時(如 100000個)倒信,就會不斷的做分配/釋放堆空間的操作,這會造成多在的性能消耗呀泳梆!
有什么辦法可以解決這個問題呢鳖悠?這就要用到我們今天要講的移動構造函數了。
移動構造函數的使用
從C++11開始优妙,類中不光可以有構造函數乘综、拷貝構造函數,還增加了一種新的構造函數即移動構造函數套硼。移動構造函數起什么作用呢卡辰?就像它的名子一樣,它可以實現指針的移動邪意,即可以將一個對象中的指針成員轉移給另一個對象九妈。指針成員轉移后,原對象中的指針成員一般要被設置為NULL雾鬼,防止其再被使用萌朱。
還是以我們上面的代碼為例,如果我們有了移動構造函數策菜,那么在將A對象push到vector時晶疼,vector內部雖然還是會再分A對象,但在進行數據的拷貝時就不是深拷貝了做入,而變成了淺拷貝冒晰,這樣就大大提高了程序的執(zhí)行效率。
如何為A增加移動構造函數呢竟块?我們來看一下代碼:
class A {
public:
...
A(A && a){
std::cout << "A move construct ..." << std::endl;
ptr_ = a.ptr_;
a.ptr_ = nullptr;
}
...
};
在 A 類中增加上面代碼即可壶运,上面的代碼看起來與普通構造函數好像沒什么兩樣,但你細心觀察可以發(fā)現該構造函數的參數是 A && a
浪秘。咦蒋情!&&
這在以前還真沒見過對吧。它表示的是C++中的右值耸携,也就是只有創(chuàng)建A對象時傳入的是右值才會執(zhí)行該構造函數
對于右值后面我們還會做詳細介紹棵癣,現在我們只要知道要想讓這個函數起作用,就必須傳給它一個右值就可以了夺衍。如么問題來了狈谊,我們這個例子中如何傳遞給它一個右值呢?這就要用到 std::move 函數了。
std::move可以將任何一值變成右值河劝,所以我們不管3721壁榕,在創(chuàng)建A對象時直接調用std::move”造”個右值給它就好了。于是我們修改main代碼如下:
int main(int argc, char *argv[]){
std::vector<A> vec;
vec.push_back(std::move(A()));
}
經這樣修后赎瞎,我們運行一下程序看現在它的結果是什么樣子吧牌里。結果如下:
A construct... //main中創(chuàng)建A對象
A move construct ... //vector內部通過移動構造函數創(chuàng)建A對象,減少了對堆空間的頻繁操作
A deconstruct ... //釋放vector中的A對象
A deconstruct ... //釋放main中創(chuàng)建的A對象
從上面的結果我們可以看出我們新增加的移動構造函數確實被調用了务甥,這樣就大大減了頻繁對堆空間的分配/釋放操作牡辽,從而提高了程序的執(zhí)行效率。這里需要注意的是敞临,在移動構造函數操作之后原A對象的指針地址已經指向NULL了态辛,因此此時就不能再通過其訪問之前的堆空間了。
C++的左值與右值
右值是C++從C繼承來的概念哟绊,最初是指=號右邊的值因妙。但現在C++中的右值已經與它最初的概念完全不一樣了痰憎。在C++中右值指的的臨時值或常量票髓,更準確的說法是保存在CPU寄存器中的值為右值,而保存在內存中的值為左值铣耘。
比如說一個常數5洽沟,我們在使用它時不會在內存中為其分配一個空間,而是直接把它放到寄存器中蜗细,所以它在C++中就是一個右值裆操。再比如說我們定義了一個變量 a,它在內存中會分配空間炉媒,因此它在C++中就是左值踪区。那么a+5
是左值還是右值呢?當然是右值對吧吊骤,因為a+5的結果存放在寄存器中缎岗,它并沒有在內存中分配新空間,所以它是右值白粉。
通過上面的描述你就應該對 C++ 中的左值和右值比較清楚了传泊。我們來看個例子吧:
#include<iostream>
int main(int argc, char *argv[]){
int && a = 5; // 正確,5會被直接存放在寄存器中鸭巴,所以它是右值
int b = 10;
int && c = b; // 錯誤眷细,b在內存中有空間,所以是左值鹃祖;左值不能賦值給左值
int && d = b + 5; // 正確溪椎,雖然 b 在內存中,但 b+5 的結果放在寄存器中,它沒有在內存中分配空間校读,因此是右值
}
在C++中使用&&
表示右值引用奔害,在上面的例子中,我首先將常數5賦值給右值引用a地熄,因為常數5是右值华临,所以這條語句可以編譯成功;緊接著我定義了變量b端考,因為它是左值雅潭,所以當將b賦值給右直引用c時,編譯器會報錯却特;最后一行將b+5
賦值給右值引用d扶供,由于b+5
不會在內存中占用空間所以這也是右值,因此最后一句編譯也沒有任何問題裂明。
接下來我們看一個有意思的情況椿浓,代碼如下:
int && e = a;
這種情況是否是合法的呢?實際上當你這么做的時候編譯器會報錯闽晦,因為a是左值而e必須接收右值扳碍。那有沒有辦法將一個左值轉成右值呢?這個問題我們前面其實已經回答過了仙蛉,通過std::move就可以解決這個問題笋敞。我們來看一個例子:
int && e = std::move(a);
之前我們直接將a賦值給e是肯定不行的,但上面的操作編譯器就不會報錯了荠瘪,因為通過std::move可以將一個左值轉成右值夯巷。但這里有一點需要特別注意:e雖然接收的必須是右值,但它本身是左值哀墓。換句話說e是一種特殊的變量趁餐,它是只能接收右值的變量。我們再從左值的本質來看篮绰,e也是占內存空間的后雷,所以它肯定是左值。
std::move的實現
上面我們已經看到了std::move的神奇之處阶牍,你可能很好奇std::move是如何做到的呢喷面?實際上std::move就是一個類型轉換器,將左值轉換成右值而以走孽。我們來看一下它的實現吧惧辈!
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
std::move的實現還是挺簡單的就這么幾行代碼,但要理解這幾行代碼可不容易磕瓷。下面我們就來對它做下詳細分析盒齿。
通用引用
首先我們來看一下move的輸入參數念逞,move的輸入參數類型稱為通用引用類型。什么是通用引用呢边翁?就是它既可以接收左值也可以接收右值翎承。我們來看一下例子:
#include<iostream>
template<typename T>
void f(T&& param){
std::cout << "the value is "<< param << std::endl;
}
int main(int argc, char *argv[]){
int a = 123;
auto && b = 5; //通用引用,可以接收右值
int && c = a; //錯誤符匾,右值引用叨咖,不能接收左值
auto && d = a; //通用引用,可以接收左值
const auto && e = a; //錯誤啊胶,加了const就不再是通用引用了
func(a); //通用引用甸各,可以接收左值
func(10); //通用引用,可以接收右值
}
在上面代碼中有兩種類型的通用引用: 一種是auto焰坪,另一種是通過模板定義的T&&趣倾。實際上auto就是模板中的T,它們是等價的某饰。下面我們就對這段代碼做下詳細解讀儒恋。
代碼中的 a 是個左值,因為它在內存中會分配空間黔漂,這應該沒什么異義诫尽;b 是通過引用。為什么呢瘟仿?因為通用引用有兩個條件:一箱锐,必須是T&&的形式比勉,由于auto等價于T劳较,所以auto && 符合這個要求;二浩聋,T類型要可以推導观蜗,也就是說它必須是個模板,而auto是模板的一種變型衣洁,因此b是通用引用墓捻。通用引用即可以接收左值,也可以接收右值坊夫,所以b=5是正確的砖第;c不是通用引用,因為它不符合T&&的形式环凿。所經第三行代碼是錯誤的梧兼,右值引用只能接收右值;d是通用引用智听,所以給它賦值a是正確的羽杰;e不是通用引用渡紫,它多了一個const已不符合T&& 的形式,所以給它左值肯定會出錯考赛;最后兩個函數調用的形參符合 T&&惕澎,又因是模板可以進行類型推導,所以是通用引用颜骤,因此給它傳左值和右值它都能正確接收唧喉。
模板的類型推導
通用引用好強大呀!它既可以接收左值又可以接收右值忍抽,它是如何做到的呢欣喧?這就要講講模板的類型推導了。
模板的類型推導規(guī)則還是蠻復雜的梯找,這里我們只簡要說明一下唆阿,有興趣的同學可以查一下C++11的規(guī)范。我們還是舉個具體的例子吧:
template <typename T>
void f(ParamType param);
f(expr);
上面這個例子是函數模板的通用例子锈锤,其中T是根據f函數的參數推到出來的驯鳖,而ParamType則是根據 T 推導出來的。T與ParamType有可能相等久免,也可能不等浅辙,因為ParamType是可以加修飾的。我們看下面的例子:
template <typename T>
void f(T param);
template <typename T>
void func(T& param);
template <typename T>
void function(T&& param);
int main(int argc, char *argv[]) {
int x = 10; // x是int
int & rr = x; // rr是 int &
const int cx = x; // cx是const int
const int& rx = x; // rx是const int &
int *pp = &x; // pp是int *
//下面是傳值的模板阎姥,由于傳入參數的值不影響原值记舆,所以參數類型退化為原始類型
f(x); // T是int
f(cx); // T是int
f(rx); // T是int
f(rr); // T是int
f(pp); // T是int*,指針比較特殊呼巴,直接使用
//下面是傳引用模板, 如果輸入參數類型有引用泽腮,則去掉引用;如果沒有引用,則輸入參數類型就是T的類型
func(x); // T為int
func(cx); // T為const int
func(rx); // T為const int
func(rr); // T為int
func(pp); // T是int*衣赶,指針比較特殊诊赊,直接使用
//下面是通用引用模板,與引用模板規(guī)則一致
function(x); // T為int&
function(5); // T為int
}
上面代碼中可以將類型推導分成兩大類:其中類型不是引用也不是指針的模板為一類; 引用和指針模板為另一類府瞄。
對于第一類其推導時根據的原則是碧磅,函數參數傳值不影響原值,所以無論你實際傳入的參數是普通變量遵馆、常量還是引用鲸郊,它最終都退化為不帶任何修修飾的原始類型。如上面的例子中货邓,const int &
類型傳進去后秆撮,退化為int型了。
第二類為模板類型為引用(包括左值引用和右值引用)或指針模板逻恐。這一類在類型推導時根據的原則是去除對等數量的引用符號像吻,其它關鍵字照般峻黍。還是我們上面的例子,func(x)
中x的類型為 int&
拨匆,它與T&
放在一起可以知道T為int姆涩。另一個例子function(x)
,其中x為int&
它與T&& 放在一起可知T為int&
惭每。
根據推導原則骨饿,我們可以知道通用引用最終的結果是什么了,左值與通用引用放在一推導出來的T仍為左值台腥,而右值與通用引用放在一起推導出來的T仍然為右值宏赘。
move 的返回類型
實際上上面通過模板推導出的T與move的返回類型息息相關的,要講明白這一點我們先要把move的返回類型弄明白黎侈。下面我們就來討論一下move的返回類型:
typename remove_reference<T>::type&&
move的返回類型非常奇特察署,我們在開發(fā)時很少會這樣寫,它表示的是什么意思呢峻汉?
這就要提到C++的另外一個知識點贴汪,即類型成員。你應該知道C++的類成員有成員函數休吠、成員變量扳埂、靜態(tài)成員三種類型,但從C++11之后又增加了一種成員稱為類型成員瘤礁。類型成員與靜態(tài)成員一樣阳懂,它們都屬于類而不屬于對象,訪問它時也與訪問靜態(tài)成員一樣用::
訪問柜思。
了解了這點岩调,我們再看move的返類型是不是也不難理解了呢?它表達的意思是返回remove_reference類的type類型成員酝蜒。而該類是一個模板類誊辉,所以在它前面要加typename關鍵字。
remove_reference看著很陌生亡脑,接下來我們再分析一下remove_reference類辈双,看它又起什么作用吧课舍。其實,通過它的名子你應該也能猜個大概了拇派,就是通過模板去除引用拍屑。我們來看一下它的實現吧途戒。
template <typename T>
struct remove_reference{
typedef T type; //定義T的類型別名為type
};
template <typename T>
struct remove_reference<T&> //左值引用
{
typedef T type;
}
template <typename T>
struct remove_reference<T&&> //右值引用
{
typedef T type;
}
上面的代碼就是remove_reference類的代碼,在C++中struct與class基本是相同的僵驰,不同點是class默認成員是private喷斋,而struct默認是public唁毒,所以使用struct代碼會寫的更簡潔一些。
通過上面的代碼我們可以知道星爪,經過remove_reference處理后浆西,T的引用被剔除了。假設前面我們通過move的類型自動推導得到T為int&&顽腾,那么再次經過模板推導remove_reference的type成員近零,這樣就可以得出type的類型為int了。
remove_reference利用模板的自動推導獲取到了實參去引用后的類型〕ぃ現在我們再回過來看move函數的時候是不是就一目了解了呢久信?之前無法理解的5行代碼現然變成了這樣:
int && move(int&& && t){
return static_case<int&&>(t);
}
//或
int && move(int& && t){
return static_case<int&&>(t);
}
經上面轉換后,我們看這個代碼就清晰多了漓摩,從中我們可以看到move實際上就是做了一個類型的強制轉換裙士。如果你是左值引用就強制轉換成右值引用。
引用折疊
上面的代碼我們看起來是簡單了很多管毙,但其參數int& &&
和int && &&
還是讓人覺得很別扭潮售。因為C++編譯器根本就不支持這兩種類型。咦锅风!這是怎么回事兒呢酥诽?
到這里我們就要講到最后一個知識點引用折疊了。在C++中根本就不存 int& &&
皱埠、int && &&
這樣的語法肮帐,但在編譯器內部是能將它們識別出來的。換句話說边器,編譯器內部能識別這種格式训枢,但它沒有給我們提供相應的接口(語法)。
實際上忘巧,當編譯器遇到這類形式的時候它會使用引用折疊技術恒界,將它們變成我們熟悉的格式。其規(guī)則如下:
-
int & &
折疊為int&
-
int & &&
折疊為int&
-
int && &
折疊為int&
-
int && &&
折疊為int &&
總結一句話就是左值引用總是折疊為左值引用砚嘴,右值引用總是折疊為右值引用十酣。
經過這一系列的操作之后,對于一個具體的參數類型int & a
际长,std::move就變成了下面的樣子:
int && move(int& t){
return static_case<int&&>(t);
}
這一下我們就清楚它在做什么事兒了哈耸采!
小結
以上就是C++高階知識移動構造函數及其原理的分析。在本文中我首先向你介紹了拷貝構造函數在某些場景下會引起程序性能嚴重下降工育,然后講解了如何使用移動構造函數和std::move函數改善性能虾宇。在文章的最后,我?guī)闵钊肫饰隽藄td::move是如何實現的如绸,最終我們發(fā)現它原來就是實現了一個自適應類型的強制類型轉換的功能嘱朽。