1. 程式人生 > >C++11:深入理解右值引用,move語義和完美轉發

C++11:深入理解右值引用,move語義和完美轉發

深入右值引用,move語義和完美轉發

轉載請註明:http://blog.csdn.net/booirror/article/details/45057689

乍看起來,move語義使得你可以用廉價的move賦值替代昂貴的copy賦值,完美轉發使得你可以將傳來的任意引數轉發給 其他函式,而右值引用使得move語義和完美轉發成為可能。然而,慢慢地你發現這不那麼簡單,你發現std::move並沒有move任何東西,完美轉發也並不完美,而T&&也不一定就是右值引用……

move語義

最原始的左值和右值定義可以追溯到C語言時代,左值是可以出現在賦值符的左邊和右邊,然而右值只能出現在賦值符的右邊。在C++裡,這種方法作為初步判斷左值或右值還是可以的,但不只是那麼準確了。你要說C++中的右值到底是什麼,這真的很難給出一個確切的定義。你可以對某個值進行取地址運算,如果不能得到地址,那麼可以認為這是個右值。例如:

int& foo();
foo() = 3; //ok, foo() is an lvalue

int bar();
int a = bar(); // ok, bar() is an rvalue

為什麼要move語義呢?它可以讓你寫出更高效的程式碼。看下面程式碼:

string foo();
string name("jack");
name = foo();

第三句賦值會呼叫string的賦值操作符函式,發生了以下事情:

  1. 首先要銷燬name的字串吧
  2. 把foo()返回的臨時字串拷貝到name吧
  3. 最後還要銷燬foo()返回的臨時字串吧

這就顯得很不高效,在C++11之前,你要些的高效點,可以是swap交換資源。C++11的move語義就是要做這事,這時過載move賦值操作符

string& string::operator=(string&& rhs);

move語義不僅僅用於右值,也用於左值。標準庫提供了std::move方法,將左值轉換成右值。因此,對於swap函式,我們可以這樣實現:

template<class T>
void swap(T& a, T& b)
{
    T temp(std::move(a));
    a = std::move(b);
    b = std::move(temp);
}

右值引用

string&& 這個型別就是所謂的右值引用,而把T&稱之為左值引用。注意,不要見到T&&就認為是右值引用,例如,下面這個就不是右值引用:

T&& foo = T(); //右值引用
auto&& bar = foo; // 不是右值引用

實際上,T&&有兩種含義,一種就是常見的右值引用;另一種是即可以是右值引用,也可以是左值引用,Scott Meyers把這種稱為Universal Reference,後來C++委員把這個改成forwarding reference,畢竟forwarding reference只在某些特定上下文才出現。

有了右值引用,C++11增加了move構造和move賦值。考慮這個情況:

void foo(X&& x)
{
  // ...
}

那麼問題來了,x的型別是右值引用,指向一個右值,但x本身是左值還是右值呢?C++11對此做出了區分:

Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.

由此可知,x是個左值。考慮到派生類的move構造,我們因這樣寫才正確:

Derived(Derived&& rhs):Base(std::move(rhs) //std::move不可缺
{ ... }

有一點必須明白,那就是std::move不管接受的引數是lvalue,還是rvalue都返回rvalue。因此我們可以給出std::move的實現如下(很接近於標準實現):

template <class T>
typename remove_reference<T>::type&& move(T&& t) 
{
    using RRefType = typename remove_reference<T>::type&&;
    return static_cast<RRefType>(t);
}

完美轉發

假設有一個函式foo,我們寫出如下函式,把接受到的引數轉發給foo:

template<class T>
void fwd(TYPE t)
{
    foo(t);
}

我們一個個來分析:

  • 如果TYPE是T的話,假設foo的引數引用型別,我會修改傳進來的引數,那麼fwd(t)和foo(t)將導致不一樣的效果。
  • 如果TYPE是T&的話,那麼fwd傳一個右值進來,沒法接受,編譯出錯。
  • 如果TYPE是T&,而且過載個const T&來接受右值,看似可以,但如果多個引數呢,你得來個排列組合的過載,因此是不通用的做法。

你很難找到一個好方法來實現它,右值引用的引入解決了這個問題,在這種上下文時,它成為forwarding reference。 這就涉及到兩條原則。第一條原則是引用摺疊原則:

  • A& & 摺疊成 A&
  • A& && 摺疊成 A&
  • A&& & 摺疊成 A&
  • A&& && 摺疊成 A&&

第二條是特殊模板引數推導原則:

1.如果fwd傳進的是個A型別的左值,那麼T被決議為A&。 2.如果fwd傳進的是個A型別的右值,那麼T被決議為A。

將兩條原則結合起來,就可以實現完美轉發。

A x; 
fwd(x); //推匯出fwd(A& &&) 摺疊後fwd(A&)

A foo();
fwd(foo());//推匯出fwd(A&& &&) 摺疊後 fwd(A&&)

std::forward應用於forwarding reference,程式碼看起來如下:

template<class T>
void fwd(T&& t)
{
    foo(std::forward<T>(t));
}

要想展開完美轉發的過程,我們必須寫出forward的實現。接下來就嘗試forward該如何實現,分析一下,std::forward是條件cast的,T的推導型別取決於傳參給t的是左值還是右值。因此,forward需要做的事情就是當且僅當右值傳給t時,也就是當T推導為非引用型別時,forward需要將t(左值)轉成右值。forward可以如下實現:

template<class T>
T&& forward(typename remove_reference<T>::type& t)
{
    return static_cast<T&&>(t);
}

現在來看看完美轉發是怎麼工作的,我們預期當傳進fwd的引數是左值,從forward返回的是左值引用;傳進的是右值,forward返回的是右值引用。假設傳給fwd是A型別的左值,那麼T被推導為A&:

void fwd(A& && t)
{
    foo(std::forward<A&>(t));
}

forward<A&>例項化:

A& && forward(typename remove_reference<A&>::type& t)
{
    return static_cast<A& &&>(t);
}

引用摺疊後:

A& forward(A& t)
{
    return static_cast<A&>(t);
}

可見,符合預期。再看看傳入fwd是右值時,那麼T被推導為A:

void fwd(A && t)
{
    foo(std::forward<A>(t));
}

forward<A>例項化如下:

A&& forward(typename remove_reference<A>::type& t)
{
    return static_cast<A&&>(t);
}

也就是:

A&& forward(A& t)
{
    return static_cast<A&&>(t);
}

forward返回右值引用,很好,完全符合預期。

總結

C++11之前,auto_ptr不能放入容器中,C++11的move語義解決了這個問題,unique_ptr就是auto_ptr的替代版。如果宣告個指向引用的引用型別的變數,比如你寫出如下程式碼:

int a = 3;
auto & & b = a;

這是不合法,編譯器會報錯。再看看完美轉發:

void f(vector<int> vi;
f({1,2,3});//ok
fwd({1,2,3})//error

還有些其他情況,你需要明白,完美轉發也不完美。