std::move(expr)和std::forward(expr)參數推導的疑問?

最近在看boost::move,裡邊涉及到boost::move(expr)和boost::forward(expr),十分不解。百度了一下,網上說move()函數可以把表達式轉化為右值,而forward()函數在某些情況下可以把表達式轉化為右值。但是查看兩個函數的源代碼,發現只有細小的區別:

雖然代碼比較複雜。。感覺區別是不是只有形參一個是左值引用,一個是右值引用?感覺好奇怪。。

有沒有大神可以具體解釋一下??


std::move是無條件的轉為右值引用,而std::forward是有條件的轉為右值引用,更準確的說叫做Perfect forwarding(完美轉發),而std::forward裡面蘊含著的條件則是Reference Collapsing(引用摺疊)。

對於std::move來說,其boost的實現基本上等價於如下形式、

template &
decltype(auto) move(T param)
{
using return_type = std::remove_reference&::type;
return static_cast&(param);
}

於是,我們可以看見這裡面的邏輯其實是無論你的param是何種類型,都會被強制轉為右值引用類型。

而唯一這裡需要注意的是模版這裡的T 類型,我願意欣賞與接受Meyers的叫法,他把這樣的類型叫做Universal Reference。對於Universal Reference來說,若你傳遞的param是一個左值,那麼T將會被deduce成Lvalue Reference(左值引用),其Param Type也是左值引用。若你傳遞進來的param是右值,那麼T則是正常的param類型,如int等,其Param Type結果是T。

舉一個簡單的栗子

template&
void foo(T param);

int i = 7;
foo(i);
foo(47);

i是一個左值,於是T被deduce成int,於是變為了

foo(int );

而整個參數的結果類型,即Param Type為int,C++不允許reference to reference,會進行引用摺疊,這也是後面談到的forward的核心。

而對於foo(47),由於47是右值,那麼T被正常的deduce成int,於是變為了

foo(int );

那麼,現在來看看forward和引用摺疊。

對於forward,其boost的實現基本可以等價於這樣的形式:

template &
T forward(typename remove_reference&::type param)
{
return static_cast&(param);
}

那麼這裡面是如何達到完美轉發的呢?

舉一個栗子

template&
void foo(T fparam)
{
std::forward&(fparam);
}

int i = 7;
foo(i);
foo(47);

如上文所述,這裡的i是一個左值,於是,我們在void foo(T fparam)這裡的話,T將會被deduce成int 然後Param Type為int。(注意,我這裡使用的變數名字為fparam,以便與forward的param進行區分)

那麼為什麼Param Type會是int呢?因為按照正常的deduce,我們將會得到

void foo(int fparam);

先前我簡單的提到了一句,C++不允許reference to reference,然而事實上,我們卻會出現Lvalue reference to Rvalue reference[1], Lvalue reference to Lvalue reference[2], Rvalue reference to Lvalue reference[3], Rvalue reference to Rvalue reference[4]等四種情況,那麼針對這樣的情況,編譯器將會根據引用摺疊規則變為一個Single Reference,那麼是左值引用還是右值引用呢?其實這個規則很簡單,只要有一個是左值引用,那麼結果就是左值引用,其餘的就是右值引用。於是我們知道了[1][2][3]的結果都是左值引用,只有[4]會是右值引用,而要從

void foo(T fparam)

這裡T的Universal Reference讓fparam擁有右值引用類型,那麼則需要保證傳遞歸來的參數為右值才可以,因為若是左值的話,T會deduce成左值引用,結合引用摺疊規則,fparam的類型會是左值引用類型。

於是我們現在來看,int 這樣的情況屬於Lvalue reference to Rvalue reference,結果則為左值引用。那麼,我們這個時候帶入到forward函數來看看,首先是T變為了int,經過了remove_reference變為了int,結合後面跟上的,則變為了int。然後我們再次替換 static_cast和return type的T為int,都得到了int

int forward(int param)
{
return static_cast&(param);
}

於是再應用引用摺疊規則,int 都劃歸為了int

int forward(int param)
{
return static_cast&(param);
}

於是,我們可以發現我們fparam變數的左值引用類型被保留了下來。這裡也需要注意,我們到達forward的時候就已經是左值引用了,所以forward並沒有改變什麼。

如我們這時候是47這樣的右值,我們知道了T會被deduce成int,經過了remove_reference,變為了int,跟上後面的,成為了int,然後再次替換static_cast和返回類型的T為int

int forward(int param)
{
return static_cast&

於是,我們也可以發現,我們fparam變數的右值引用類型也完美的保留了下來。

更權威和詳細的解釋樓主可以閱讀Effective Modern C++,那是本好書,我也只是把Meyers的觀點簡要的提煉了一下。


Perfect forwarding and universal references in C++

請認真閱讀。重點是reference collapsing


@藍色 的答案正確,不過真的太長了。我試著簡化一下。

首先 move 和 forward 不是用於轉換引用類型, remove_reference + static_cast 才是。

move 用於指明轉移語義,常見於傳參數和返回值。move 用於提示編譯器,這裡可以是轉移語義,否則編譯器會保守的使用複製語義。

forward 用於轉調其它函數不改變參數的類型(引用)。這個問題在C++98時就已經有過了,你不能把函數對象的模板參數寫成void operater()(T test),因為會出現T = T時test = T 的情況。

C++11的答案是使用轉調前把參數放在forward中,可以保證test的類型為T。

實現原理上,C++比較黑暗,用到了引用的引用等於引用,不過你可以不管它。


推薦閱讀:

C++ delete[] 是如何知道數組大小的?
面向對象編程(oop)從誕生到現在理念上經歷了幾次怎樣大的變遷和轉化?
C++ 11為什麼引入nullptr?
為什麼判斷 std::vector 是否為空時,用 if(0==vec.size()) 提示效率低,但用 if (vec.empty()) 正常?
C/C++中char/int/long等基本內置類型為何要編譯器相關而不是固定長度?

TAG:編程語言 | C | 編譯器 | C標準 | C11 |