引子-深拷貝和淺拷貝
????在cpp11之前,我們定義一個(gè)類如果類中有指針成員,并且其指向一塊堆內(nèi)存,那么往往本類要負(fù)責(zé)這個(gè)指針指向內(nèi)存的分配和銷毀,不然會(huì)產(chǎn)生令人討厭的內(nèi)存泄露問題。但如果這個(gè)類的對(duì)象之間進(jìn)行復(fù)制就會(huì)涉及到數(shù)據(jù)的拷貝問題,如果不加處理,使用編譯器默認(rèn)生成的拷貝構(gòu)造函數(shù),那么默認(rèn)的行為就是按bit進(jìn)行memory copy,這就是所謂的淺拷貝。淺拷貝的后果是兩個(gè)對(duì)象持有兩個(gè)不同的成員指針,兩個(gè)指針指向同樣的堆內(nèi)存,如果其中一個(gè)銷毀了內(nèi)存,另一個(gè)再用,就會(huì)產(chǎn)生無法預(yù)知的錯(cuò)誤,導(dǎo)致程序錯(cuò)誤。所以針對(duì)這種情況我們往往要提供自定義版本的拷貝構(gòu)造函數(shù),來確保指針指向的內(nèi)存也有一份拷貝。可以看一下代碼示例:
class PtrMemTest
{
public:
int* p;
PtrMemTest(int value)
{
p = new int(value);
}
~PtrMemTest()
{
if (p)
{
delete p;
p = nullptr;
}
}
PtrMemTest(const PtrMemTest& obj)
{
p = new int(*obj.p);
}
};
????如果我們不定義拷貝構(gòu)造函數(shù),那么就會(huì)引起淺拷貝的問題,繼而可能引起嚴(yán)重的程序錯(cuò)誤。提供了拷貝構(gòu)造函數(shù)之后,我們保證了程序的正確性,但是內(nèi)存的頻繁拷貝是一種性能開銷,如果允許我們?cè)谝恍┣闆r下只是把指針的指向的內(nèi)存所有權(quán)轉(zhuǎn)移應(yīng)該如何呢?比如下面的代碼
PtrMemTest obj2(PtrMemTest(12));
????這里只是用一個(gè)臨時(shí)對(duì)象去初始化另一個(gè)對(duì)象,會(huì)產(chǎn)生兩次堆內(nèi)存的申請(qǐng),其中一次實(shí)際完全沒有必要,因?yàn)槭莻€(gè)臨時(shí)對(duì)象,完成了第二個(gè)對(duì)象的初始化之后就馬上銷毀了。這個(gè)例子比較極端,在平時(shí)程序中不常見,那么用一個(gè)函數(shù)返回值去初始化一個(gè)對(duì)象的場(chǎng)景就極為常見了。如代碼
PtrMemTest GetMemObj(int value)
{
return PtrMemTest(value);
}
void testMem()
{
PtrMemTest obj3(GetMemObj(13));
}
????這種情況下跟上面情況類似,函數(shù)的返回值作為一個(gè)臨時(shí)對(duì)象,去初始化obj3這個(gè)對(duì)象,完成使命后就自行銷毀,產(chǎn)生了不必要或者說可以優(yōu)化的內(nèi)存拷貝。這種情況實(shí)際上會(huì)產(chǎn)生一次構(gòu)造和兩次拷貝構(gòu)造(實(shí)際編譯器會(huì)對(duì)這種情況做優(yōu)化,從而不會(huì)產(chǎn)生額外的內(nèi)存釋放和銷毀),一次是PtrMemTest(value)產(chǎn)生的構(gòu)造,一次是函數(shù)GetMemObj中產(chǎn)生的一個(gè)臨時(shí)對(duì)象作為函數(shù)的返回值,而它用剛才的對(duì)象進(jìn)行構(gòu)造,從而產(chǎn)生了一次拷貝構(gòu)造,最后一次拷貝構(gòu)造發(fā)生在構(gòu)造obj3的時(shí)候。那么如果可以有效利用臨時(shí)對(duì)象,把它們的內(nèi)存“偷過來“就可以減少一次有可能成本十分昂貴的拷貝構(gòu)造。在cpp11中,有了對(duì)這些場(chǎng)景的一個(gè)解決方案--右值引用。
什么是右值
????在c中我們可以近似的認(rèn)為賦值號(hào)左邊的稱為左值(lvalue)右邊的稱為右值(rvalue)。如
int a = 3;
int b = a + 5;
????其中a和b就是左值,3和a+5都是右值。
????還有一個(gè)比較被廣泛認(rèn)同的定義,可以被合法取地址的值稱為左值,反之稱為右值。如&a, &b都是合法表達(dá)式,所以他們都是左值;但是&3,&(a+5)都是不合法的表達(dá)式,所以他們不能取地址,進(jìn)而它們即是右值。那么容易看出來函數(shù)的返回值是一個(gè)臨時(shí)值,無法被取地址,所以是個(gè)右值。
右值引用
????cpp11中右值引用就是對(duì)一個(gè)右值進(jìn)行引用的類型。由于右值通常不具有名字,我們也只能通過引用的方式綁定它。如
int&& a = ReturnIntRvalue();
int&& b = 12;
int& c = ReturnIntRvalue();//compile fail
const int& d = ReturnIntRvalue();
????上面代碼中前兩句都是右值引用綁定右值的例子,右值引用只能綁定到右值上,否則會(huì)編譯失敗。值得一提的是,第三句無法編譯通過,由于cpp不允許左值引用綁定到右值上,但是第四句卻能編譯通過,原因是const T&類型是萬能類型,可以綁定到任何類型,左值,常量左值,右值。不過這里相對(duì)于右值引用,它是只讀的,而右值引用是可以改變所引用的右值的值的。
再看一下代碼
class PtrMemTest
{
public:
int* p;
PtrMemTest(int value)
{
std::cout<<"PtrMemTest"<<std::endl;
p = new int(value);
}
~PtrMemTest()
{
std::cout<<"~PtrMemTest"<<std::endl;
if (p)
{
delete p;
p = nullptr;
}
}
PtrMemTest(const PtrMemTest& obj)
{
std::cout<<"PtrMemTest copy"<<std::endl;
p = new int(*obj.p);
}
PtrMemTest(PtrMemTest&& obj)
{
std::cout<<"PtrMemTest move"<<std::endl;
p = obj.p;
obj.p = nullptr;
}
};
static void execute()
{
PtrMemTest obj1(111);
PtrMemTest obj2(std::move(obj1));
}
????這里對(duì)前面的代碼做了增加,增加了一個(gè)cpp11中新增的移動(dòng)構(gòu)造函數(shù),它的作用正是前面我們希望的將一個(gè)右值(臨時(shí)對(duì)象)的內(nèi)容“偷”過來,用最小的代價(jià)來構(gòu)造新的對(duì)象。move這個(gè)庫函數(shù),用來強(qiáng)制將一個(gè)值轉(zhuǎn)換成右值。所以這里運(yùn)行的結(jié)果是
PtrMemTest
PtrMemTest move
~PtrMemTest
~PtrMemTest
????obj2將自己的成員指針指向了obj1中開辟的內(nèi)存,obj1此后變成了一個(gè)空對(duì)象。這里為了規(guī)避編譯器的優(yōu)化,所以寫成了這種方式,這種方式是有風(fēng)險(xiǎn)的,因?yàn)樵趏bj2構(gòu)造后,程序就不應(yīng)該再使用obj1了,否則可能出現(xiàn)問題。更合理的用法是
PtrMemTest obj2(GetPtrMemObj());
????這樣函數(shù)產(chǎn)生的臨時(shí)返回值就會(huì)被“移動(dòng)”到obj2中,從而減少了內(nèi)存的分配和銷毀。
移動(dòng)語義
????前面的例子所展示即為移動(dòng)語義。標(biāo)準(zhǔn)庫中提供了一個(gè)有用的函數(shù)std::move來強(qiáng)制將左值轉(zhuǎn)換成右值,正如剛才例子中所展示的。有了這個(gè)標(biāo)準(zhǔn)庫函數(shù),我們可以更加靈活的按自己的需求來將左值轉(zhuǎn)換成右值。比如繼承一個(gè)有移動(dòng)構(gòu)造的基類,子類并不增加數(shù)據(jù),只是擴(kuò)展了一些函數(shù),那么如果此時(shí)需要提供自己的版本的移動(dòng)構(gòu)造函數(shù)以延續(xù)父類的移動(dòng)語義,這時(shí)你可能就明確需要move函數(shù)了。如下代碼
class PtrMemTest
{
public:
int* p;
PtrMemTest(int value)
{
std::cout<<"PtrMemTest"<<std::endl;
p = new int(value);
}
~PtrMemTest()
{
std::cout<<"~PtrMemTest"<<std::endl;
if (p)
{
delete p;
p = nullptr;
}
}
PtrMemTest(const PtrMemTest& obj)
{
std::cout<<"PtrMemTest copy"<<std::endl;
p = new int(*obj.p);
}
PtrMemTest(PtrMemTest&& obj)
{
std::cout<<"PtrMemTest move"<<std::endl;
p = obj.p;
obj.p = nullptr;
}
};
class PtrMemTestDerive : public PtrMemTest
{
public:
PtrMemTestDerive(int value) : PtrMemTest(value)
{
std::cout<<"PtrMemTestDerive"<<std::endl;
}
PtrMemTestDerive(const PtrMemTestDerive &obj) : PtrMemTest(obj)
{
std::cout<<"PtrMemTestDerive copy"<<std::endl;
}
PtrMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(obj)
{
std::cout<<"PtrMemTestDerive move"<<std::endl;
}
//........
};
void test()
{
PtrMemTestDerive obj1(111);
PtrMemTestDerive obj3(std::move(obj1));
}
????這段代碼并沒有按照你所期望的行使移動(dòng)語義,因?yàn)镻trMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(obj)雖然子類中傳入了右值引用,但是將obj像父類的移動(dòng)構(gòu)造(其實(shí)是拷貝構(gòu)造)傳遞的時(shí)候,obj是個(gè)左值,所以并沒有如預(yù)期的調(diào)用父類的移動(dòng)構(gòu)造函數(shù),而是調(diào)用了父類的拷貝構(gòu)造函數(shù)。所以這里move可以顯示它的作用了。
class PtrMemTestDerive : public PtrMemTest
{
public:
PtrMemTestDerive(int value) : PtrMemTest(value)
{
std::cout<<"PtrMemTestDerive"<<std::endl;
}
PtrMemTestDerive(const PtrMemTestDerive &obj) : PtrMemTest(obj)
{
std::cout<<"PtrMemTestDerive copy"<<std::endl;
}
PtrMemTestDerive(PtrMemTestDerive &&obj) : PtrMemTest(std::move(obj))
{
std::cout<<"PtrMemTestDerive move"<<std::endl;
}
//........
};
新的子類用了move將obj繼續(xù)作為右值傳遞給父類,順利的調(diào)用父類的移動(dòng)構(gòu)造函數(shù)。
std::move
????上面的示例用到了很多STL庫提供的新函數(shù)move函數(shù),前面提到過的它的作用是強(qiáng)制將左值轉(zhuǎn)換成右值引用。注意返回的是一個(gè)右值引用,而不是右值。但是直接使用move的返回值即得到一個(gè)右值,而不是一個(gè)右值引用。比較繞,看一下STL庫中的注釋:
Move as rvalue
Returns an rvalue reference to arg.
This is a helper function to force move semantics on values, even if they have a name: Directly using the returned value causes arg to be considered an rvalue.
大概翻譯一下就是返回一個(gè)右值引用,直接用它的返回值就被當(dāng)成一個(gè)右值用。
它的應(yīng)用場(chǎng)景前面已經(jīng)演示了,那么我們簡(jiǎn)單看一下它的實(shí)現(xiàn)。
template <class _Tp>
inline
typename remove_reference<_Tp>::type&&
move(_Tp&& __t)
{
typedef typename remove_reference<_Tp>::type _Up;
return static_cast<_Up&&>(__t);
}
????上面代碼去掉了一些宏定義,留下了骨干內(nèi)容,下面分析一下這個(gè)很短小的函數(shù)。首先它是一個(gè)模板函數(shù),行參_Tp&& __t這里并不代表是_Tp類型的右值引用,而是有其特定規(guī)則的,這里后續(xù)在完美轉(zhuǎn)發(fā)語義中會(huì)詳細(xì)講到,我們目前只需認(rèn)為這里經(jīng)過推倒后_Tp的類型可能是左值,左值引用,常量左值引用,和右值引用就好了。看下面的函數(shù)體通過字面意思remove_reference這個(gè)模板類的唯一作用是萃取出_Tp的原始非引用類型。也就是說_Up是個(gè)左值類型,這樣最后返回的很明確,不管傳入什么,最后傳出都是一個(gè)右值引用。
????關(guān)于move我們?cè)诤罄m(xù)的完美轉(zhuǎn)發(fā)中還會(huì)繼續(xù)分析。