Modern C++ - Move
移動語義是C++爲了提高資源管理效率而在C++11新增的特性。
它透過右值引用(T&&)、move和移動構造等方法實現。
⌈移動⌋並不真的轉移資源,而是轉移了資源的所有權與生命週期。
爲什麼需要移動語義
拷貝 時 原對象與新對象同時存在且相互獨立。
這造成了大量的資源浪費與性能開銷。
移動語義的引入就是爲了解決該問題的。
移動語義如何避免拷貝開銷
移動的本質是轉移所有權與生命週期,所以它實際上等同於這樣:
int main(){
T t1;
T t2;
t2.data = t1.data; //手動將指標的所有權轉移
t1.data = nullptr; //將原對象置空,表示沒有了原始數據的所有權
//... 如果還有其他成員一樣的操作
}即便沒有移動語義,也能夠做到類似的行爲。
但C++將其加入到了語言標準,使其更爲直觀規範並成爲了語言特性的一部分。
移動語義不僅方便且直觀,還更能表達出要轉移資源的意思:
int main(){
T t1;
T t2 = std::move(t1);
}值型別
C++中,有左值和右值兩種值型別。
值型別可以用於區分是否進行移動操作。
左值(left value),指向某個記憶體位址的具名對象,可以被取位址。
右值(right value),通常表示臨時對象和字面量,沒有具名的內存位址,無法被取位址。
10; //這就是一個字面量右值,無具體名稱。
int x; // x 就是一個左值,它有一個變數名 x,可以被取位址理解值類別對於掌握移動語義非常重要。
值類別與值型別是兩種概念,且它們同時存在int; //這是值類型
10; //這是一個值型別爲右值 類型爲int的對象
int x = 10; //類型爲int的右值,被賦值到了類型爲int的變數具名 x。移動語義的實現
爲了實現移動語義,C++11中新增了類型 右值引用(T&&),與之相對的便是左值引用(T&)。
std::move實際上做的是將左值轉換爲右值,具體來說實現可能類似這樣:
template<T>
T&& std::move(T&& val) noexcept {
return static_cast<T&&>(val);
}同時,要爲類支援移動構造只需要重載一個接收右值的構造函式即可:
struct Test{
int * data;
Test(){};
~Test(){ delete data;};
Test(Test && other) noexcept {//移動構造函式,你需要在這裏實現對移動的支援
data = other.data;
other.data = nullptr; //置空原對象,表示原對象沒有了資源的所有權與避免重複釋放
}
};
int main(){
Test t1;
Test t2 = std::move(t1);
}移動也伴隨着生命週期的轉移
int main(){
Test t1;
{ //一個作用域
Test t2; // t2.data記憶體位址0x01,資源的生命週期到該作用域(花括號)結束
t1 = std::move(t2); // 移動後,0x01的生命週期被綁定到了t1
}
} // t1生命週期結束,此時0x01才被釋放可以參閱RAII的說明。
Modern C++ - RAII & 如何保證記憶體安全
ID:152024-11-20
未定義(Undefined behavior)但可解構,其不應再被使用 (即便它可能有效)。未定義行爲表示任何可能,程式的行爲不受任何保證。 它可能可以按照你的預期運行,可能是一個無用值,或是引發程序崩潰。
不少初學者
難以理解的問題,都是因爲使用到了未定義行爲。引用摺疊、拷貝消除和完美轉發
移動語義增加了右值引用,並由此衍生出了一系列的特性與方法。引用摺疊、拷貝消除與完美轉發是其中之一。
引用摺疊
引用摺疊用於在模板編程,引用類型的組合會被 ⌈摺疊⌋成對應的類型。
它的規則大致如下:
T& &或T& &&或T&& &會摺疊爲T&。T&& &&會摺疊爲T&&。
這意味著:一個左值引用和任何引用組合結果都是左值引用,而右值引用和右值引用組合結果依然是右值引用。
範例:
template<T>
void func(T&& val){ //val的類型取決與傳入的是左值還是右值
}
int main(){
int x = 10;
func(x); // 這裏 T 是 int& (左值),val類型展開是int& && 並摺疊爲 int&(左值) 類型。
func(10); //這裏 T 是 int&& (右值),val類型展開爲int&& &&,然後摺疊爲int &&(右值) 類型。
}在這個例子中,如果傳入左值,T會被推斷為int&,而如果傳入右值,T就是int。
拷貝消除
拷貝消除是C++標準規定的編譯器優化技術,用來減少不必要的對象拷貝。
其適用與以下兩種場景:
- 返回值優化(Return Value Optimization, RVO):當函式傳回一個
臨時對象時,拷貝消除允許編譯器直接在返回對象的內存位置構造對象。從而避免多餘的拷貝。 - NRVO(Named Return Value Optimization):NRVO是RVO的一種特殊情況,當函式返回
具名對象時,編譯器仍可消除拷貝。
template<T>
T make(){
return T(); // RVO: 編譯器會直接在 main 函數中構造 T() 的對象
}
template<T>
T make2(){
T res; // NRVO: 編譯器會優化 res,直接在 main 函數中構造該對象
return res;
}
int main(){
int x = make<int>(); // 使用 RVO
int x2 = make2<int>(); // 使用 NRVO
}RVO與NRVO的區別在於是否返回具名對象。
值得注意的是,當函式返回的對象生命週期不超過函式結束才能觸發NRVO
T func(){
T res; // 這是一個⌈臨時對象⌋,因爲其生命週期等於函式結束
return res; // 可以觸發NRVO
}
T t1;
T func2(){
return t1; // 無法觸發NRVO,因爲返回的對象生命週期超過函式結束
}
T func3(){
static T t2;
return t2; // 這裏也一樣,無法觸發NRVO
}
完美轉發
引用摺疊是 完美轉發(Perfect Forwarding) 的基礎。完美轉發允許模板函數不改變參數的值型別,直接轉發給其他函數 (避免參數在多個函式中傳遞引用規則變更值型別)。
這是透過右值引用和std::forward實現的。
template<T>
void func1(T && val){
}
template<T>
void func2(T&& val){
//...進行一些操作,然後需要調用func1
func1(std::forward<T>(val)); // 使用完美轉發避免值型別發生變更
}小結
移動語義是C++爲了提高資源管理效率新增的特性。
它的實現依賴右值引用(T&&)、move和移動構造,使資源的所有權可以被有效轉移。
這帶來了以下幾個重要的相關特性:
值型別用於區分是否進行移動操作。引用摺疊用於自動組合並區分在模板編程中的值類別。拷貝消除是標準化的移動語義實現優化,進一步提高性能,完美轉發用於參數在多個函式間傳遞時,避免因傳遞過程變更值類別。
參閱:
选材新颖独特,通过细节描写赋予主题鲜活生命力。