1. 程式人生 > >C++11改進我們的程式之move和完美轉發

C++11改進我們的程式之move和完美轉發

轉:http://www.cnblogs.com/qicosmos/p/3376241.html

本次要講的是右值引用相關的幾個函式:std::move, std::forward和成員的emplace_back,通過這些函式我們可以避免不必要的拷貝,提高程式效能。move是將物件的狀態或者所有權從一個物件轉移到另一個物件,只是轉移,沒有記憶體的搬遷或者記憶體拷貝。如圖所示是深拷貝和move的區別。


  這種移動語義是很有用的,比如我們一個物件中有一些指標資源或者動態陣列,在物件的賦值或者拷貝時就不需要拷貝這些資源了。在c++11之前我們的拷貝建構函式和賦值函式可能要這樣定義:
假設一個A物件內部有一個資源m_ptr;

A& A::operator=(const A& rhs)
{
// 銷燬m_ptr指向的資源
// 複製rhs.m_ptr所指的資源,並使m_ptr指向它
}

同樣A的拷貝建構函式也是這樣。假設我們這樣來用A:

A foo(); // foo是一個返回值為X的函式
A a;
a = foo();

最後一行有如下的操作:

  • 銷燬a所持有的資源
  • 複製foo返回的臨時物件所擁有的資源
  • 銷燬臨時物件,釋放其資源

  上面的過程是可行的,但是更有效率的辦法是直接交換a和臨時物件中的資源指標,然後讓臨時物件的解構函式去銷燬a原來擁有的資源。換句話說,當賦值操作符的右邊是右值的時候,我們希望賦值操作符被定義成下面這樣:

A& A::operator=(const A&& rhs)
{
// 僅僅轉移資源的所有者,將資源的擁有者改為被賦值者
}

  這就是所謂的move語義。再看一個例子,假設一個臨時容器很大,賦值給另一個容器。

複製程式碼
{
std::list< std::string > tokens;//省略初始化...
std::list< std::string > t = tokens;
}
std::list< std::string > tokens;
std::list< std::string > t = std::move(tokens);
複製程式碼

  如果不用std::move,拷貝的代價很大,效能較低。使用move幾乎沒有任何代價,只是轉換了資源的所有權。如果一個物件內部有較大的對記憶體或者動態陣列時,很有必要寫move語義的拷貝建構函式和賦值函式,避免無謂的深拷貝,以提高效能。

完美轉發

  在上一篇的博文中我介紹了右值引用,右值引用型別是獨立於值的,一個右值引用引數作為函式的形參,在函式內部再轉發該引數的時候它已經變成一個左值了,並不是它原來的型別了。因此,我們需要一種方法能按照引數原來的型別轉發到另一個函式,這種轉發被稱為完美轉發。所謂完美轉發(perfect forwarding),是指在函式模板中,完全依照模板的引數的型別,將引數傳遞給函式模板中呼叫的另外一個函式。c++11中提供了這樣的一個函式std::forward,它是為轉發而生的,它會按照引數本來的型別來轉發出去,不管引數型別是T&&這種未定的引用型別還是明確的左值引用或者右值引用。看看這個例子。

複製程式碼
template<typename T>
void PrintT(T& t)
{
cout << "lvaue" << endl;
}

template<typename T>
void PrintT(T && t)
{
cout << "rvalue" << endl;
}

template<typename T>
void TestForward(T && v)
{
PrintT(v);
PrintT(std::forward<T>(v));
PrintT(std::move(v));
}

Test()
{
TestForward(1);
int x = 1;
TestForward(x);
TestForward(std::forward<int>(x));
}
複製程式碼

  測試結果:


  我們來分析一下測試結果:

  • TestForward(1);由於1是右值,所以未定的引用型別T && v被一個右值初始化後變成了一個右值引用,但是在TestForward函式體內部,呼叫PrintT(v);時,v又變成了一個左值,因為它這裡已經變成了一個具名的變數,所以它是一個左值,因此第一個PrintT被呼叫,打印出"lvaue";PrintT(std::forward<T>(v));由於std::forward會按引數原來的型別轉發,因此,這時它還是一個右值(這裡已經發生了型別推導,所以這裡的T&&不是一個未定的引用型別,關於這點可以參考我的上一篇講右值引用的博文),所以會呼叫void PrintT(T &&t)函式。PrintT(std::move(v));是將v變成一個右值引用,雖然它本來也是右值引用,因此它和PrintT(std::forward<T>(v));的輸出結果是一樣的。
  • TestForward(x);未定的引用型別T && v被一個左值初始化後變成了一個左值引用,因此在呼叫PrintT(std::forward<T>(v));它會轉發到void PrintT(T& t);

萬能的函式包裝器

  右值引用、完美轉發再結合可變模板引數,我們可以寫一個萬能的函式包裝器,它可以接收所有的函式,帶返回值的、不帶返回值的、帶引數的和不帶引數的函式都可以委託這個萬能的函式包裝器執行。看看這個萬能的函式包裝器。

複製程式碼
template<class Function, class... Args>
inline auto FuncWrapper(Function && f, Args && ... args) -> decltype(f(std::forward<Args>(args)...))
{
//typedef decltype(f(std::forward<Args>(args)...)) ReturnType;
return f(std::forward<Args>(args)...);
//your code; you can use the above typedef.
}
複製程式碼

再看看測試程式碼:

複製程式碼
void test0()
{
cout << "void" << endl;
}

int test1()
{
return 1;
}

int test2(int x)
{
return x;
}

string test3(string s1, string s2)
{
return s1 + s2;
}

test()
{
FuncWrapper(test0);  //沒有返回值,列印1
FuncWrapper(test1); //返回1
FuncWrapper(test2, 1); //返回1
FuncWrapper(test3, "aa", "bb"); //返回"aabb"
}
複製程式碼

成員的emplace_back

  c++11中大部分容器都加了一個emplace_back成員函式,vector中它的定義是這樣的:

template< class... Args >
void emplace_back( Args&&... args );

  這裡的Args&&是一個未定的引用型別,因此它可以接收左值引用和右值引用,它的內部也是呼叫了std::forward實現完美轉發的。因此如果我們需要往容器中新增右值、臨時變數時,用emplace_back可以提高效能。