濟南品牌營銷型網(wǎng)站建設(shè)品牌策劃運營公司
移動語義和右值引用
現(xiàn)在介紹本書前面未討論的主題。C++11 支持移動語義,這就提出了一些問題:為何需要移動語義?什么是移動語義?C++11 如何支持它?下面首先討論第一個問題。
為何需要移動語義
先來看 C++11 之前的復(fù)制過程。假設(shè)有如下代碼:
vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
...
vector<string> vstr_copy1(vstr); // make vstr_copy1 a copy of vstr
vector 和 string 類都使用動態(tài)內(nèi)存分配,因此它們必須定義使用某種 new 版本的復(fù)制構(gòu)造函數(shù)。為初始化對象 vstr_copy1,復(fù)制構(gòu)造函數(shù) vector<string> 將使用 new 給 20000 個 string 對象分配內(nèi)存,而每個string對象又將調(diào)用 string 的復(fù)制構(gòu)造函數(shù),該構(gòu)造函數(shù)使用 new 為 1000 個字符分配內(nèi)存。接下來,全部 20000000 個字符都將從 vstr 控制的內(nèi)存中復(fù)制到 vstr_copy1 控制的內(nèi)存中。這里的工作量很大,但只要妥當(dāng)就行。
但這確實妥當(dāng)嗎?有時候答案是否定的。例如,假設(shè)有一個函數(shù),它返回一個 vector<string> 對象:
vector<string> allcaps(const vector<string> & vs) {vector<string> temp;// code that stores an all-uppercase version of vs in tmepreturn temp;
}
接下來,假設(shè)以下面這種方式使用它:
vector<string> vstr;
// build up a vector of 20,000 strings, each of 1000 characters
vector<string> vstr_copy1(vstr); // #1
vector<string> vstr_copy2(allcaps(vstr)); // #2
從表面上看,語句 #1 和 #2 類似,它們都使用一個現(xiàn)有的對象初始化一個 vector<string> 對象。如果深入探索這些代碼,將發(fā)現(xiàn) allcaps() 創(chuàng)建了對象 temp,該對象管理著 20000000 個字符;vector 和 string 的復(fù)制構(gòu)造函數(shù)創(chuàng)建這 20000000 個字符的副本,然后程序刪除 allcaps() 返回的臨時對象(遲鈍的編譯器甚至可能將 temp 復(fù)制給一個臨時返回對象,刪除 temp,再刪除臨時返回對象)。這里的要點是,臨時對象被復(fù)制后被刪了。如果編譯器將對數(shù)據(jù)的所有權(quán)直接從temp轉(zhuǎn)讓給 vstr_copy2,不是更好嗎?也就是說,不將 20000000 個字符復(fù)制到新地方,再刪除原來的字符,而將字符留在原來的地方,并將 vstr_copy2 與之相關(guān)聯(lián)。這類似于在計算機中移動文件的情形:實際文件還留在原來的地方,而只修改記錄。這種方法被稱為移動語義(move semantics)。有點和字面意思看起來相悖的是,移動語義實際上避免了移動原始數(shù)據(jù),而只是修改了記錄。
要實現(xiàn)移動語義,需要采取某種方式,讓編譯器知道什么時候需要復(fù)制,什么時候不需要。這就是右值引用發(fā)揮作用的地方??啥x兩個構(gòu)造函數(shù)。其中一個是常規(guī)復(fù)制構(gòu)造函數(shù),它使用 const 左值引用作為參數(shù),這個引用關(guān)聯(lián)到左值實參,如 語句#1 中的 vstr;另一個是移動構(gòu)造函數(shù),它使用右值引用作為參數(shù),該引用關(guān)聯(lián)到右值實參,如語句 #2 中的 allcaps(vstr) 的返回值。復(fù)制構(gòu)造函數(shù)可執(zhí)行深復(fù)制,而移動構(gòu)造函數(shù)只調(diào)整記錄。在將所有權(quán)轉(zhuǎn)移給新對象的過程中,移動構(gòu)造函數(shù)可能修改其實參,這意味著右值引用參數(shù)不應(yīng)是 const。
一個移動示例
下面通過一個示例演示移動語義和右值引用的工作原理。下面的程序定義并使用了 Useless 類。這個類動態(tài)分配內(nèi)存,并包含常規(guī)復(fù)制構(gòu)造函數(shù)和移動構(gòu)造函數(shù),其中移動構(gòu)造函數(shù)使用了移動語義和右值引用。為演示流程,構(gòu)造函數(shù)和析構(gòu)函數(shù)都比較啰嗦,同時 Useless 類還使用了一個靜態(tài)變量來跟蹤對象數(shù)量。另外,省略了一些重要的方法,如賦值運算符。
// useless.cpp -- an otherwise useless class with move semantics
#include<iostream>
using namespace std;// interface
class Useless {
private:int n; // number of elementschar * pc; // pointer to datastatic int ct; // number of objectsvoid ShowObject() const;
public:Useless();explicit Useless(int k);Useless(int k, char ch);Useless(const Useless & f); // regular copy constructorUseless(Useless && f); // move constructor~Useless();Useless operator+(const Useless & f) const;// need operator=() in copy and move versionsvoid ShowData() const;
};// implematation
int Useless::ct = 0;Useless::Useless() {++ct;n = 0;pc = nullptr;cout << "default constructor called; number of objects: " << ct << endl;ShowObject();
}Useless::Useless(int k) : n(k) {++ct;cout << "int constructor called; number of objects: " << ct << endl;pc = new char[n];ShowObject();
}Useless::Useless(int k, char ch) : n(k) {++ct;cout << "int, char constructor called; number of objects: " << ct << endl;pc = new char[n];for(int i = 0; i < n; i++){pc[i] = ch;}ShowObject();
}Useless::Useless(const Useless & f): n(f.n) {++ct;cout << "copy constructor called; number of objects: " << ct << endl;pc = new char[n];for (int i = 0; i < n; i++){pc[i] = f.pc[i];}ShowObject();
}Useless::Useless(Useless && f) : n(f.n) {++ct;cout << "move constructor called; number of objects: " << ct << endl;pc = f.pc; // steal addressf.pc = nullptr; // give old object nothing in returnf.n = 0;ShowObject();
}Useless::~Useless() {cout << "destructor called; objects left: " << --ct << endl;cout << "deleted object:\n";ShowObject();delete [] pc;
}Useless Useless::operator+(const Useless &f) const {cout << "Entering operator+()\n";Useless temp = Useless(n+f.n);for (int i = 0; i < n; i++) {temp.pc[i] = pc[i];}for (int i = n; i < temp.n; i++){temp.pc[i] = f.pc[i-n];}cout << "temp object:\n";cout << "Leaving operator+()\n";return temp;
}void Useless::ShowObject() const {cout << "Number of element: " << n;cout << " Data address: " << (void *) pc << endl;
}void Useless::ShowData() const {if (n == 0 ) {cout << "(object empty)";}else {for (int i = 0; i< n; i++){cout << pc[i];}}cout << endl;
}// application
int main() {{Useless one(10, 'x');Useless two = one; // calss copy constructorUseless three(20, 'o');Useless four (one + three); // calls operator+(), move contructorcout << "object one: ";one.ShowData();cout << "object two: ";two.ShowData();cout << "object three: ";three.ShowData();cout << "object four: ";four.ShowData();}
}
其中最重要的是復(fù)制構(gòu)造函數(shù)和移動構(gòu)造函數(shù)的定義。首先來看復(fù)制構(gòu)造函數(shù)(刪除了輸出語句):
Useless::Useless(const Useless & f) : n(f.n) {++ct;pc = new char[n];for (int i = 0; i < n; i++ ) {pc[i] = f.pc[i];}
}
它執(zhí)行深復(fù)制,是下面的語句將使用的構(gòu)造函數(shù):
Useless two = one; // calls copy constructor
引用 f 將指向左值對象 one。
接下來看移動構(gòu)造函數(shù),這里也刪除了輸出語句:
Useless::Useless(Useless && f) : n(f.n) {++ct;pc = f.pc; // steal addressf.pc = nullptr; // give old object nothing in returnsf.n = 0;
}
它讓 pc 指向現(xiàn)有的數(shù)據(jù),以獲取這些數(shù)據(jù)的所有權(quán)。此時,pc 和 f.pc 指向相同的數(shù)據(jù),調(diào)用析構(gòu)函數(shù)時這將帶來麻煩,因為程序不能對同一個地址調(diào)用 delete[] 兩次。為避免這種問題,該構(gòu)造函數(shù)隨后將原來的指針設(shè)置為空指針,因為對空指針執(zhí)行 delete[] 沒有問題。這種奪取所有權(quán)的方式常被稱為竊取(pilfering)。上述代碼還將原始對象的元素設(shè)置為零,這并非必不可少的,但讓這個示例的輸出更一致。注意,由于修改了 f 對象,這要求不能在參數(shù)聲明中使用 const。
在下面的語句中,將使用這個構(gòu)造函數(shù):
Useless four (one + three); // calls move constructor
表達式 one+three 調(diào)用 Useless::operator+(),而右值引用 f 將關(guān)聯(lián)到該方法返回的臨時對象。
下面是在 MicroSoft Visual C++ 2010 中編譯時,該程序的輸出:
int, char constructor called; number of objects: 1
Number of element: 10 Data address: 0xabe2c0
copy constructor called; number of objects: 2
Number of element: 10 Data address: 0xabe2e0
int, char constructor called; number of objects: 3
Number of element: 20 Data address: 0xabe300
Entering operator+()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0xabe320
temp object:
Leaving operator+()
move constructor called; number of objects: 5
Number of elements: 30 Data address: 0xabe320
destructor called; objects left: 4
deleted object:
Number of elements: 0 Data address: 00000000
object one: xxxxxxxxxx
object two: xxxxxxxxxx
object three: oooooooooooooooooooo
object four: xxxxxxxxxxoooooooooooooooooooo
destructor called; objects left: 3
deleted object:
Number of element: 30 Data address: 0xabe320
destructor called; objects left: 2
deleted object:
Number of element: 20 Data address: 0xabe300
destructor called; objects left: 1
deleted object:
Number of element: 10 Data address: 0xabe2e0
destructor called; objects left: 0
deleted object:
Number of element: 10 Data address: 0xabe2c0
注意到對象 two 是對象 one 的副本:他們顯示的數(shù)據(jù)輸出相同,但顯示的數(shù)據(jù)地址不同。另一方面,在方法 Useless::operator+() 中創(chuàng)建的對象的數(shù)據(jù)地址與對象 four 存儲的數(shù)據(jù)地址相同,其中對象 four 是由移動復(fù)制構(gòu)造函數(shù)創(chuàng)建的。另外,注意到創(chuàng)建對象 four 后,為臨時對象調(diào)用了析構(gòu)函數(shù)。之所以知道這是臨時對象,是因為其元素和數(shù)據(jù)地址都是0.
如果使用編譯器 g++4.5.0 和 標(biāo)記 -std=c++11 編譯該程序,輸出將不同,這很有趣:
int, char constructor called; number of objects: 1
Number of element: 10 Data address: 0xabe2c0
copy constructor called; number of objects: 2
Number of element: 10 Data address: 0xabe2e0
int, char constructor called; number of objects: 3
Number of element: 20 Data address: 0xabe300
Entering operator+()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0xabe320
temp object:
Leaving operator+()
object one: xxxxxxxxxx
object two: xxxxxxxxxx
object three: oooooooooooooooooooo
object four: xxxxxxxxxxoooooooooooooooooooo
destructor called; objects left: 3
deleted object:
Number of element: 30 Data address: 0xabe320
destructor called; objects left: 2
deleted object:
Number of element: 20 Data address: 0xabe300
destructor called; objects left: 1
deleted object:
Number of element: 10 Data address: 0xabe2e0
destructor called; objects left: 0
deleted object:
Number of element: 10 Data address: 0xabe2c0
注意到?jīng)]有調(diào)用移動構(gòu)造函數(shù),且只創(chuàng)建了 4 個對象。創(chuàng)建對象 four 時,該編譯器沒有調(diào)用任何構(gòu)造函數(shù);相反,它推斷出對象 four 是 operator+() 所做的工作的受益人,因此將 operator()+創(chuàng)建的對象轉(zhuǎn)到 four 的名下。一般而言,編譯器完全可以進行優(yōu)化,只要結(jié)果與未優(yōu)化時相同。即使您省略該程序中的移動構(gòu)造函數(shù),并使用 g++ 進行編譯,結(jié)果也將相同。
移動構(gòu)造函數(shù)解析
雖然使用右值引用可支持移動語義,但這并不會神奇地發(fā)生。要讓移動語義發(fā)生,需要兩個步驟。首先,右值引用讓編譯器知道何時可使用移動語義:
Useless two = one; // matches Useless::Useless(const Useless &)
Useless four ( one + three); // matches Useless::Useless(Useless &&)
對象 one 是左值,與左值引用匹配,而表達式 one+three 是右值,與右值引用匹配。因此右值引用讓編譯器使用移動構(gòu)造函數(shù)來初始化對象 four。實現(xiàn)移動語義的第二步是,編寫移動構(gòu)造函數(shù),使其提供所需的行為。
總之,通過提供一個使用左值引用的構(gòu)造函數(shù)和一個使用右值引用的構(gòu)造函數(shù),將初始化分成了兩組。使用左值對象初始化對象時,將使用復(fù)制構(gòu)造函數(shù),而使用右值對象初始化對象時,將使用移動構(gòu)造函數(shù)。程序員可根據(jù)需要賦予這些構(gòu)造函數(shù)不同的行為。
這就帶來了一個問題:在引入右值引用前,情況是什么樣的呢?如果沒有移動構(gòu)造函數(shù),且編譯器未能通過優(yōu)化消除對復(fù)制構(gòu)造函數(shù)的需求,結(jié)果將如何呢?在 C++98 中,下面的語句將調(diào)用復(fù)制構(gòu)造函數(shù):
Useless four (one + three);
但左值引用不能指向右值。結(jié)果將如何呢?第 8 章介紹過,如果實參為右值,const 引用形參將指向一個臨時變量:
int twice(const int & rx) {return 2 * rx;
}
...
int main() {int m = 6;// below, rx refers to mint n = twice(m);// below, rx refers to a temporary variable initialized to 21int k = twice(21);
}
就 Useless 而言,形參 f 將被初始化一個臨時對象,而該臨時對象被初始化為 operator+() 返回的值。下面是使用老式編譯器進行編譯時,之前的程序(刪除了移動構(gòu)造函數(shù))的部分輸出:
Entering operator+()
int constructor called; number of objects: 4
Number of element: 30 Data address: 0x1785320
temp object:
Leaving operator+()
copy constructor called; number of objects: 5
Number of element: 30 Data address: 0x1785340
destructor called; objects left: 4
deleted object:
Number of element: 30 Data address: 0x1785320
copy constructor called; number of objects: 5
Number of element: 30 Data address: 0x1785320
destructor called; objects left: 4
deleted object:
Number of element: 30 Data address: 0x1785340
首先,在方法 Useless::operator+()內(nèi),調(diào)用構(gòu)造函數(shù)創(chuàng)建了 temp,并在 0x1785320 給它分配了存儲 30 個元素的空間。然后,調(diào)用復(fù)制構(gòu)造函數(shù)創(chuàng)建了一個臨時復(fù)制信息(其地址為 0x1785340),f 指向該副本。接下來,刪除了地址為 0x1785320 的對象 temp。然后,新建了對象 four,它使用了 0x1785320 處剛釋放的內(nèi)存。接下來,刪除了 0x1785340 處的臨時參數(shù)對象。這表明,總共創(chuàng)建了三個對象,但其中的兩個被刪除。這些就是移動語義旨在消除的額外工作。
正如 g++ 示例表明的,機智的編譯器可能自動消除額外的復(fù)制工作,但通過使用右值引用,程序員可以指出何時該使用移動語義。
賦值
適用于構(gòu)造函數(shù)的移動語義考慮也適用于賦值運算符。例如,下面演示了如何給 Useless 類編寫復(fù)制賦值運算符和移動賦值運算符:
Useless & Useless::operator=(const Useless & f) { // copy assignmentif (this == &if) return *this;delete [] pc;n = f.n;pc = new char[n];for ( int i = 0; i < n; i++ ) {pc[i] = f.pc[i];}return *this;
}Useless & Useless::operator=(Useless && f) { // move assignmentif (this == &f) {return *this;}delete [] pc;n = f.n;pc = f.pc;f.n = 0;f.pc= nullptr;return *this;
}
上述賦值運算符采用了第12章介紹的常規(guī)模式,而移動賦值運算符刪除目標(biāo)對象中的原始數(shù)據(jù),并將源對象的所有權(quán)轉(zhuǎn)讓給目標(biāo)。不能讓多個指標(biāo)指向相同的數(shù)據(jù),這很重要,因此上述代碼將源對象中的指針設(shè)置為空指針。
與移動構(gòu)造函數(shù)一樣,移動賦值運算符的參數(shù)也不能是 const 引用,因為這個方法修改了源對象。
強制移動
移動構(gòu)造函數(shù)和移動賦值運算符使用右值。如果要讓它們使用左值,該如何辦呢?例如,程序可能分析一個包含候選對象的數(shù)組,選擇其中一個對象供以后使用,并丟棄數(shù)組。如果可以使用移動構(gòu)造函數(shù)或移動賦值運算符來保留選定的對象,那該多好啊。然而,假設(shè)您試圖像下面這樣做:
Useless choices[10];
Useless best;
int pick;
... // select one object, set pick to index
best = choices[pick];
由于 choices[pick] 是左值,因此上述賦值語句將使用復(fù)制賦值運算符,而不是移動賦值運算符。但如果能讓 choices[pick] 看起來像右值,變將使用移動賦值運算符。為此,可使用運算符 static_cast<> 將對象的類型強制轉(zhuǎn)換為 Useless &&,但 C++ 提供了一種更簡單的方式——使用頭文件 utility 中聲明的函數(shù) std::move()。下面的程序演示了這種技術(shù),它在 Useless 類中添加了啰嗦的賦值運算符,并讓以前啰嗦的構(gòu)造函數(shù)和析構(gòu)函數(shù)保持沉默。
// stdmove.cpp -- using std::move()
#include<iostream>
#include<utility>// interface
class Useless {
private:int n; // number of elementschar * pc; // pointer to datastatic int ct; // number of objectsvoid ShowObject() const;
public:Useless();explicit Useless(int k);Useless(int k, char ch);Useless(const Useless & f); // regular copy constructorUseless(Useless && f); // move constructor~Useless();Useless operator+(const Useless & f) const;Useless & operator=(const Useless & f); // copy assignmentUseless & operator=(Useless && f); // move assignmentvoid ShowData() const;
};// implementation
int Useless::ct = 0;Useless::Useless() {++ct;n = 0;pc = nullptr;
}Useless::Useless(int k) : n(k) {++ct;pc = new char[n];
}Useless::Useless(int k, char ch) : n(k) {++ct;pc = new char[n];for (int i = 0; i < n; i++ ){pc[i] = ch;}
}Useless::Useless(const Useless & f): n(f.n) {++ct;pc = new char[n];for(int i = 0; i < n; i++ ){pc[i] = f.pc[i];}
}Useless::Useless(Useless && f) : n(f.n) {++ct;pc = f.pc; // steal addressf.pc = nullptr; // give old object nothing in returnf.n = 0;
}Useless::~Useless() {delete [] pc;
}Useless & Useless::operator=(const Useless & f) { // copy assignmentstd::cout << "copy assignment operator called:\n";if (this == &f){return *this;}delete[] pc;n = f.n;pc = new char[f.n];for (int i = 0; i < n; i++){pc[i] = f.pc[i];}return *this;
}Useless & Useless::operator=(Useless && f) { // move assignmentstd::cout << "move assignment operator called:\n";if (this == &f) {return *this;}delete [] pc;n = f.n;pc = f.pc;f.n = 0;f.pc = nullptr;return *this;
}Useless Useless::operator+(const Useless &f) const {Useless temp = Useless(n + f.n);for (int i = 0; i< n; i++){temp.pc[i] = pc[i];}for (int i = n; i < temp.n; i++){temp.pc[i] = f.pc[i-n];}return temp;
}void Useless::ShowObject() const {std::cout << "Number of elements: " << n;std::cout << " Data address: " << (void *) pc << std::endl;
}void Useless::ShowData() const {if (n == 0){std::cout << "(object empty)";}else{for (int i = 0; i < n; i++){std::cout << pc[i];}}std::cout << std::endl;
}// application
int main(){using std::cout;{Useless one(10, 'x');Useless two = one + one; // calls move contructorcout << "object one: ";one.ShowData();cout << "object two: ";two.ShowData();Useless three, four;cout << "three = one\n";three = one;cout << "now object three = ";three.ShowData();cout << "and object one = ";one.ShowData();cout << "four = one + two\n";four = one + two; // automatic move assignmentcout << "now object four = ";four.ShowData();cout << "four = move(one)\n";four = std::move(one); // forced move assignmentcout << "now object four = ";four.ShowData();cout << "and object one = ";one.ShowData();}return 0;
}
該程序的輸出如下:
object one: xxxxxxxxxx
object two: xxxxxxxxxxxxxxxxxxxx
three = one
copy assignment operator called:
now object three = xxxxxxxxxx
and object one = xxxxxxxxxx
four = one + two
move assignment operator called:
now object four = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
four = move(one)
move assignment operator called:
now object four = xxxxxxxxxx
and object one = (object empty)
正如您看到的,將 one 賦給 three 調(diào)用了復(fù)制賦值運算符,但將 move(one) 賦給 four 調(diào)用的是移動賦值運算符。
需要知道的是,函數(shù) std::move() 并非一定會導(dǎo)致移動操作。例如,假設(shè) Chunk 是一個包含私有數(shù)據(jù)的類,而您編寫了如下代碼:
Chunk one;
...
Chunk two;
two = std::move(one); // move semantics?
表達式 std::move(one) 是右值,因此上述賦值語句將調(diào)用 Chunk 的移動賦值運算符——如果定義了這樣的運算符。但如果 Chunk 沒有定義移動賦值運算符,編譯器將使用復(fù)制賦值運算符。如果也沒有定義復(fù)制賦值運算符,將根本不允許上述賦值。
對大多數(shù)程序員來說,右值引用帶來的主要好處并非是讓他們能夠編寫使用右值引用的代碼,而是能夠使用利用右值引用實現(xiàn)移動語義的庫代碼。例如,STL 類現(xiàn)在都有復(fù)制構(gòu)造函數(shù)、移動構(gòu)造函數(shù)、復(fù)制賦值運算符和移動賦值運算符。