如果你從事過C++
編程,你會對引用比較熟悉,C++
的引用允許你為已經(jīng)存在的對象創(chuàng)建一個新的名字。對新引用所做的訪問和修改操作,都會影響它的原型。
例如:
int var=42;
int& ref=var; // 創(chuàng)建一個var的引用
ref=99;
assert(var==99); // 原型的值被改變了,因為引用被賦值了
目前為止,我們用過的所有引用都是左值引用——對左值的引用。lvalue這個詞來自于C語言,指的是可以放在賦值表達式左邊的事物——在棧上或堆上分配的命名對象,或者其他對象成員——有明確的內(nèi)存地址。rvalue這個詞也來源于C語言,指的是可以出現(xiàn)在賦值表達式右側(cè)的對象——例如,文字常量和臨時變量。因此,左值引用只能被綁定在左值上,而不是右值。
不能這樣寫:
int& i=42; // 編譯失敗
例如,因為42是一個右值。好吧,這有些假;你可能通常使用下面的方式講一個右值綁定到一個const左值引用上:
int const& i = 42;
這算是鉆了標準的一個空子吧。不過,這種情況我們之前也介紹過,我們通過對左值的const引用創(chuàng)建臨時性對象,作為參數(shù)傳遞給函數(shù)。
其允許隱式轉(zhuǎn)換,所以你可這樣寫:
void print(std::string const& s);
print("hello"); //創(chuàng)建了臨時std::string對象
C++11標準介紹了右值引用(rvalue reference),這種方式只能綁定右值,不能綁定左值,其通過兩個&&
來進行聲明:
int&& i=42;
int j=42;
int&& k=j; // 編譯失敗
因此,可以使用函數(shù)重載的方式來確定:函數(shù)有左值或右值為參數(shù)的時候,看是否能被同名且對應參數(shù)為左值或有值引用的函數(shù)所重載。
其基礎(chǔ)就是C++11新添語義——移動語義(move semantics)。
右值通常都是臨時的,所以可以隨意修改;如果知道函數(shù)的某個參數(shù)是一個右值,就可以將其看作為一個臨時存儲或“竊取”內(nèi)容,也不影響程序的正確性。這就意味著,比起拷貝右值參數(shù)的內(nèi)容,不如移動其內(nèi)容。動態(tài)數(shù)組比較大的時候,這樣能節(jié)省很多內(nèi)存分配,提供更多的優(yōu)化空間。試想,一個函數(shù)以std::vector<int>
作為一個參數(shù),就需要將其拷貝進來,而不對原始的數(shù)據(jù)做任何操作。C++
03/98的辦法是,將這個參數(shù)作為一個左值的const引用傳入,然后做內(nèi)部拷貝:
void process_copy(std::vector<int> const& vec_)
{
std::vector<int> vec(vec_);
vec.push_back(42);
}
這就允許函數(shù)能以左值或右值的形式進行傳遞,不過任何情況下都是通過拷貝來完成的。如果使用右值引用版本的函數(shù)來重載這個函數(shù),就能避免在傳入右值的時候,函數(shù)會進行內(nèi)部拷貝的過程,因為可以任意的對原始值進行修改:
void process_copy(std::vector<int> && vec)
{
vec.push_back(42);
}
如果這個問題存在于類的構(gòu)造函數(shù)中,竊取內(nèi)部右值在新的實例中使用??梢詤⒖家幌虑鍐沃械睦?默認構(gòu)造函數(shù)會分配很大一塊內(nèi)存,在析構(gòu)函數(shù)中釋放)。
清單A.1 使用移動構(gòu)造函數(shù)的類
class X
{
private:
int* data;
public:
X():
data(new int[1000000])
{}
~X()
{
delete [] data;
}
X(const X& other): // 1
data(new int[1000000])
{
std::copy(other.data,other.data+1000000,data);
}
X(X&& other): // 2
data(other.data)
{
other.data=nullptr;
}
};
一般情況下,拷貝構(gòu)造函數(shù)①都是這么定義:分配一塊新內(nèi)存,然后將數(shù)據(jù)拷貝進去。不過,現(xiàn)在有了一個新的構(gòu)造函數(shù),可以接受右值引用來獲取老數(shù)據(jù)②,就是移動構(gòu)造函數(shù)。在這個例子中,只是將指針拷貝到數(shù)據(jù)中,將other以空指針的形式留在了新實例中;使用右值里創(chuàng)建變量,就能避免了空間和時間上的多余消耗。
X類(清單A.1)中的移動構(gòu)造函數(shù),僅作為一次優(yōu)化;在其他例子中,有些類型的構(gòu)造函數(shù)只支持移動構(gòu)造函數(shù),而不支持拷貝構(gòu)造函數(shù)。例如,智能指針std::unique_ptr<>
的非空實例中,只允許這個指針指向其對象,所以拷貝函數(shù)在這里就不能用了(如果使用拷貝函數(shù),就會有兩個std::unique_ptr<>
指向該對象,不滿足std::unique_ptr<>
定義)。不過,移動構(gòu)造函數(shù)允許對指針的所有權(quán),在實例之間進行傳遞,并且允許std::unique_ptr<>
像一個帶有返回值的函數(shù)一樣使用——指針的轉(zhuǎn)移是通過移動,而非拷貝。
如果你已經(jīng)知道,某個變量在之后就不會在用到了,這時候可以選擇顯式的移動,你可以使用static_cast<X&&>
將對應變量轉(zhuǎn)換為右值,或者通過調(diào)用std::move()
函數(shù)來做這件事:
X x1;
X x2=std::move(x1);
X x3=static_cast<X&&>(x2);
想要將參數(shù)值不通過拷貝,轉(zhuǎn)化為本地變量或成員變量時,就可以使用這個辦法;雖然右值引用參數(shù)綁定了右值,不過在函數(shù)內(nèi)部,會當做左值來進行處理:
void do_stuff(X&& x_)
{
X a(x_); // 拷貝
X b(std::move(x_)); // 移動
}
do_stuff(X()); // ok,右值綁定到右值引用上
X x;
do_stuff(x); // 錯誤,左值不能綁定到右值引用上
移動語義在線程庫中用的比較廣泛,無拷貝操作對數(shù)據(jù)進行轉(zhuǎn)移可以作為一種優(yōu)化方式,避免對將要被銷毀的變量進行額外的拷貝。在2.2節(jié)中看到,在線程中使用std::move()
轉(zhuǎn)移std::unique_ptr<>
得到一個新實例;在2.3節(jié)中,了解了在std:thread
的實例間使用移動語義,用來轉(zhuǎn)移線程的所有權(quán)。
std::thread
、std::unique_lock<>
、std::future<>
、 std::promise<>
和std::packaged_task<>
都不能拷貝,不過這些類都有移動構(gòu)造函數(shù),能讓相關(guān)資源在實例中進行傳遞,并且支持用一個函數(shù)將值進行返回。std::string
和std::vector<>
也可以拷貝,不過它們也有移動構(gòu)造函數(shù)和移動賦值操作符,就是為了避免拷貝拷貝大量數(shù)據(jù)。
C++標準庫不會將一個對象顯式的轉(zhuǎn)移到另一個對象中,除非將其銷毀的時候或?qū)ζ滟x值的時候(拷貝和移動的操作很相似)。不過,實踐中移動能保證類中的所有狀態(tài)保持不變,表現(xiàn)良好。一個std::thread
實例可以作為移動源,轉(zhuǎn)移到新(以默認構(gòu)造方式)的std::thread
實例中。還有,std::string
可以通過移動原始數(shù)據(jù)進行構(gòu)造,并且保留原始數(shù)據(jù)的狀態(tài),不過不能保證的是原始數(shù)據(jù)中該狀態(tài)是否正確(根據(jù)字符串長度或字符數(shù)量決定)。
在使用右值引用作為函數(shù)模板的參數(shù)時,與之前的用法有些不同:如果函數(shù)模板參數(shù)以右值引用作為一個模板參數(shù),當對應位置提供左值的時候,模板會自動將其類型認定為左值引用;當提供右值的時候,會當做普通數(shù)據(jù)使用??赡苡行┛谡Z化,來看幾個例子吧。
考慮一下下面的函數(shù)模板:
template<typename T>
void foo(T&& t)
{}
隨后傳入一個右值,T的類型將被推導為:
foo(42); // foo<int>(42)
foo(3.14159); // foo<double><3.14159>
foo(std::string()); // foo<std::string>(std::string())
不過,向foo傳入左值的時候,T會被推導為一個左值引用:
int i = 42;
foo(i); // foo<int&>(i)
因為函數(shù)參數(shù)聲明為T&&
,所以就是引用的引用,可以視為是原始的引用類型。那么foo<int&>()就相當于:
foo<int&>(); // void foo<int&>(int& t);
這就允許一個函數(shù)模板可以即接受左值,又可以接受右值參數(shù);這種方式已經(jīng)被std::thread
的構(gòu)造函數(shù)所使用(2.1節(jié)和2.2節(jié)),所以能夠?qū)⒖烧{(diào)用對象移動到內(nèi)部存儲,而非當參數(shù)是右值的時候進行拷貝。