一文帶你掌握C++中的移動語義和完美轉發(fā)
移動語義
C++11新特性的std::move()用于將一個左值轉換為右值引用。它并不是實際移動或復制數(shù)據(jù),而是通過將一個左值強制轉換為一個右值引用來實現(xiàn)對對象的轉移。這個特性在C++11中引入,用于優(yōu)化對象移動操作的效率。
我們知道,右值引用只能引用右值,如果嘗試綁定左值就會編譯錯誤。
int i = 0; int &&k = i; // 編譯錯誤
在C++11標準中可以在不創(chuàng)建臨時值的情況下顯式地將左值通過static_cast轉換為將亡值,通過值類別的內(nèi)容我們知道將亡值屬于右值,所以可以被右值引用綁定。值得注意的是,由于轉換的并不是右值,因此它依然有著和轉換之前相同的生命周期和內(nèi)存地址,例如:
int i = 0; int &&k = static_cast<int&&>(i);
既然這個轉換既不改變生命周期,也不改變內(nèi)存地址,那它存在的意義是什么?實際上它最大的作用是讓左值使用移動語義。
舉例:
#include <iostream> class BigMemoryPool { public: static const int PoolSize = 4096; BigMemoryPool() : pool_(new char[PoolSize]) { std::cout << "普通構造函數(shù)" << std::endl; } ~BigMemoryPool() { if (pool_ != nullptr) { delete[] pool_; } } BigMemoryPool(BigMemoryPool &&other) : pool_(new char[PoolSize]) { std::cout << "移動構造函數(shù)" << std::endl; pool_ = other.pool_; other.pool_ = nullptr; } BigMemoryPool(const BigMemoryPool &other) : pool_(new char[PoolSize]) { std::cout << "拷貝構造函數(shù)" << std::endl; memcpy(pool_, other.pool_, PoolSize); } private: char *pool_; }; BigMemoryPool get_pool(const BigMemoryPool &pool) { return pool; } BigMemoryPool make_pool() { BigMemoryPool pool; return get_pool(pool); } int main() { BigMemoryPool my_pool1; BigMemoryPool my_pool2 = my_pool1; BigMemoryPool my_pool3 = static_cast<BigMemoryPool &&>(my_pool1); return 0; }
在這段代碼中,my_pool1是一個BigMemoryPool類型的對象,也是一個左值,所以用它去構造my_pool2的時候調用的是復制構造函數(shù)。為了讓編譯器調用移動構造函數(shù)構造my_pool3,這里使用了static_cast<BigMemoryPool &&>(my_pool1)將my_pool1強制轉換為右值(也是將亡值,為了敘述思路的連貫性后面不再強調)。由于調用了移動構造函數(shù),my_pool1失去了自己的內(nèi)存數(shù)據(jù),后面的代碼也不能對my_pool1進行操作了。
結果輸出:
PS C:\Users\zh'n\Desktop\新建文件夾> g++ -std=c++11 -fno-elide-constructors main.cpp -o main
PS C:\Users\zh'n\Desktop\新建文件夾> ./main
普通構造函數(shù)
拷貝構造函數(shù)
移動構造函數(shù)
但是這個示例中把my_pool1這個左值轉換成my_pool3這個左值似乎沒有什么意義,而且程序員如果再次去訪問my_pool1還會引發(fā)未定義行為。
正確的使用場景是在一個右值被轉換為左值后需要再次轉換為右值,最典型的例子是一個右值作為實參傳遞到函數(shù)中。我們在討論左值和右值的時候曾經(jīng)提到過,無論一個函數(shù)的實參是左值還是右值,其形參都是一個左值,即使這個形參看上去是一個右值引用,例如:
void move_pool(BigMemoryPool &&pool) { std::cout << "call move_pool" << std::endl; BigMemoryPool my_pool(pool); } int main() { move_pool(make_pool()); }
結果輸出:
PS C:\Users\zh'n\Desktop\新建文件夾> g++ -std=c++11 -fno-elide-constructors main.cpp -o main
PS C:\Users\zh'n\Desktop\新建文件夾> ./main
普通構造函數(shù)
拷貝構造函數(shù)
移動構造函數(shù)
call move_pool
拷貝構造函數(shù)
代碼中,make_pool()返回的是一個臨時對象,也是一個右值,move_pool的參數(shù)是一個右值引用,但是在使用形參pool去構造my_pool時調用的是拷貝構造函數(shù)。如果我們想調用移動構造函數(shù)的話,需要把形參pool強制轉換為右值。
void move_pool(BigMemoryPool &&pool) { std::cout << "call move_pool" << std::endl; BigMemoryPool my_pool = static_cast<BigMemoryPool &&>(pool); // 1 }
結果輸出:
PS C:\Users\zh'n\Desktop\新建文件夾> g++ -std=c++11 -fno-elide-constructors main.cpp -o main
PS C:\Users\zh'n\Desktop\新建文件夾> ./main
普通構造函數(shù)
拷貝構造函數(shù)
移動構造函數(shù)
call move_pool
移動構造函數(shù)
請注意,在這個場景下強制轉換為右值就沒有任何問題了,因為move_pool函數(shù)的實參是make_pool返回的臨時對象,當函數(shù)調用結束后臨時對象就會被銷毀,所以轉移其內(nèi)存數(shù)據(jù)不會存在任何問題。
在C++11的標準庫中還提供了一個函數(shù)模板std::move幫助我們將左值轉換為右值,這個函數(shù)內(nèi)部也是用static_cast做類型轉換。只不過由于它是使用模板實現(xiàn)的函數(shù),因此會根據(jù)傳參類型自動推導返回類型,省去了指定轉換類型的代碼。另一方面從移動語義上來說,使用std::move函數(shù)的描述更加準確。所以建議讀者使用std::move將左值轉換為右值而非自己使用static_cast轉換,例如:
void move_pool(BigMemoryPool &&pool) { std::cout << "call move_pool" << std::endl; BigMemoryPool my_pool(std::move(pool)); // 1 }
總結:
std::move()內(nèi)部是用static_cast做類型轉換,只不過它是使用模板實現(xiàn)的函數(shù),因此會根據(jù)傳參類型自動推導返回值類型,省去了指定類型的代碼。如果使用std::move()將一個左值轉換為右值并賦值給其他對象后,這個對象就會被銷毀,所以在函數(shù)調用過程中,創(chuàng)建N個對象實際上只是把第一個對象的內(nèi)存不斷的轉移,類似層層遞歸。 這樣做的好處就是省去了創(chuàng)建對象的開銷,并且在對象副本龐大的情況下節(jié)省了大量時間。
完美轉發(fā)
在了解完美轉發(fā)之前,先了解一下什么是萬能引用和引用折疊。
我們知道常量左值引用可以引用左值,也可以引用右值,是一個幾乎的萬能引用,但是因為它的常量性導致使用受限制。
在C++11中有一個“萬能引用”,例如:
void foo(int &&i){} // 右值引用 template<class T> void bar(T &&t){} // 萬能引用 int get_val(){return 5;} int &&x = get_val(); // 右值引用 auto &&x = get_val(); // 萬能引用
我們可以發(fā)現(xiàn),只要是自動類型推導的引用就是萬能引用。在這個推導過程中,源對象是左值,那就推導為左值引用,源對象是右值,那就推導為右值引用。
萬能引用能如此靈活地引用對象,實際上是因為在C++11中添加了一套引用疊加推導的規(guī)則——引用折疊。在這套規(guī)則中規(guī)定了在不同的引用類型互相作用的情況下應該如何推導出最終類型。
舉例說明:
int i = 42; const int j = 11; bar(i); bar(j); bar(get_val()); auto &&x = i; auto &&y = j; auto &&z = get_val();
在bar(i);中i是一個左值,所以T的推導類型結果是int&,根據(jù)引用折疊規(guī)則int& &&的最終推導類型為int&,于是bar函數(shù)的形參是一個左值引用。而在bar(get_val());中get_val返回的是一個右值,所以T的推導類型為非引用類型int,于是最終的推導類型是int&&,bar函數(shù)的形參成為一個右值引用。
完美轉發(fā)的用途
看一個常規(guī)的轉發(fā)函數(shù)模板
#include <iostream> #include <string> #include <typeinfo> template<class T> void show_type(T t) { std::cout << typeid(t).name() << std::endl; } template<class T> void normal_forwarding(T t) { show_type(t); } int main() { std::string s = "hello world"; normal_forwarding(s); } // 輸出:Ss
normal_forwarding函數(shù)可以完成字符串的轉發(fā)任務,但是它的效率很慢。首先它的參數(shù)是值傳遞,那么在轉發(fā)過程中就會發(fā)生一次臨時對象的復制。其中一個解決方法就是把void normal_forwarding(T t)換成void normal_forwarding(T& t),通過引用傳遞,但這是一個左值引用,如果參數(shù)是一個右值就會編譯失敗。
std::string get_string() { return "hi world"; } normal_forwarding(get_string()); // 編譯失敗
但是常量左值可以引用右值,可以解決這個問題,但引來的新問題是常量左值引用具有常量性,使得對象不可以被修改。
所以萬能引用的誕生解決了這個問題。
對于萬能引用來說,如果實參是一個左值,那么形參會被推導為左值引用、如果實參是一個右值,那么形參會被推導為右值引用。
#include <iostream> #include <string> template<class T> void show_type(T t) { std::cout << typeid(t).name() << std::endl; } template<class T> void perfect_forwarding(T &&t) // 萬能引用 { show_type(static_cast<T&&>(t)); } std::string get_string() { return "hi world"; } int main() { std::string s = "hello world"; perfect_forwarding(s); perfect_forwarding(get_string()); }
和移動語義的情況一樣,顯式使用static_cast類型轉換進行轉發(fā)不是一個便捷的方法。在C++11的標準庫中提供了一個std::forward函數(shù)模板,在函數(shù)內(nèi)部也是使用static_cast進行類型轉換,只不過使用std::forward轉發(fā)語義會表達得更加清晰,std::forward函數(shù)模板的使用方法也很簡單:
template<class T> void perfect_forwarding(T &&t) { show_type(std::forward<T>(t)); }
請注意std::move和std::forward的區(qū)別,其中std::move一定會將實參轉換為一個右值引用,并且使用std::move不需要指定模板實參,模板實參是由函數(shù)調用推導出來的。而std::forward會根據(jù)左值和右值的實際情況進行轉發(fā),在使用的時候需要指定模板實參。
完整示例:
#include <iostream> #include <string> #include <typeinfo> template <class T> void show_type(T t) { std::cout << typeid(t).name() << std::endl; } template <class T> void perfect_forwarding(T &&t) { show_type(std::forward<T>(t)); } int main() { std::string s = "hello world"; perfect_forwarding(s); // 實參是左值 perfect_forwarding(1.0); // 實參是右值 } // 輸出 // Ss // d
總結
完美轉發(fā)允許將函數(shù)的參數(shù)(包括左值和右值)轉發(fā)給其他函數(shù),同時保持原始參數(shù)的值不變,這樣可以實現(xiàn)高效的函數(shù)調用。
#include <iostream> #include <utility> template <typename T> void process(T &i) { std::cout << "L-value: " << i << std::endl; } template <typename T> void process(T &&i) { std::cout << "R-value: " << i << std::endl; } template <typename T> void forwarder(T &&t) { process(std::forward<T>(t)); } int main() { int a = 42; forwarder(a); // L-value: 42 forwarder(7.1); // R-value: 7 return 0; } // 輸出 // L-value: 42 // R-value: 7.1
在上面的示例中,forwarder函數(shù)使用了完美轉發(fā),它接受一個泛型類型的參數(shù)T&& t,并將參數(shù)t轉發(fā)給process函數(shù)。通過使用std::forward(t),可以將原始參數(shù)的值類別(左值或右值)傳遞給process函數(shù),從而調用合適的重載函數(shù)。
通過使用完美轉發(fā),可以更好地處理函數(shù)參數(shù)的轉發(fā),避免不必要的拷貝,提高代碼的性能和效率。請注意,完美轉發(fā)需要注意避免懸垂引用和引用折疊等問題,在實際使用中需要謹慎處理。
以上就是一文帶你掌握C++中的移動語義和完美轉發(fā)的詳細內(nèi)容,更多關于C++移動語義和完美轉發(fā)的資料請關注腳本之家其它相關文章!
相關文章
基于matlab MFCC+GMM的安全事件聲學檢測系統(tǒng)
這篇文章主要為大家介紹了基于matlab MFCC+GMM的安全事件聲學檢測系統(tǒng)實現(xiàn)及源碼示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-02-02