0%

Rvalue reference, std::move and std::forward

這篇文章主要是Rvalue References: C++0x Features in VC10, Part 2的閱讀筆記,在原文撰寫過程當中,規格有所變動,因此根據最新的規則做補充說明。

Copy Problems

C++98/03的時候,最令人詬病的問題,就是建立太多臨時物件,Value Semantics的意思就是複製出來的物件跟原先是獨立的,不會互相干擾

Lvalue and Rvalue

在C++98/03時期,有這麼一條規則Every C++ expression is either an lvalue or an rvalue.,Lvalue是在運算過後留下來的續存物件,而Rvalue是運算過後生命期就結束的臨時物件。
殂此之外,C++98/03裡還有一條規則A function call is an lvalue if and only if the result type is a reference

1
2
3
4
5
6
7
8
9
string one("one");
const string two("two");
string three() { return "three"; }
const string four() { return "four"; }

one; // modifiable lvalue
two; // const lvalue
three(); // modifiable rvalue
four(); // const rvalue
  • Type&可以繫結到一個modifiable lvalue,而如果要繫結到modifiable rvalue,C++規定禁止(Visual C++除外,可以把警告層級從3調到4,會警告你這樣很危險,而gcc跟clang則是輸出錯誤訊息。)
  • const Type&可以繫結到任何一種型態,不過不能對Rvalue做任何修改,因此不能對即將消滅的臨時物件採取任何行動

而在C++11之後,引進了Rvalue reference,解決了這個問題。

  • Type&&可以繫結到一個modifiable rvalue,而不能繫結到modifiable lvalue,需要強制轉型。
  • const Type&&可以繫結任何形態

每個reference都有一個名字,所以Bind到Rvalue的refernce,他是一個Lvalue,因此以下的程式碼。

1
2
3
4
void print(const string&) { cout << "print(const string&)" << endl; }
void print(string&&) { cout << "print(string&&)" << endl; }
void RvalueTest(string&& str) { print(str); }
RvalueTest(string());

在RValueTest執行結束之前,str是個合法的物件,因此被當作Lvalue,會執行第一個print。

為了學習Rvalue的觀念,自行打造move跟forward函數。

先看C++11之後引進的Reference Collapsing Rules

  • T& + & => T&
  • T&& + & => T&
  • T& + && => T&
  • T&& + T&& => T&&

而Move的用途就是明確指出不管物件是Lvalue或Rvalue,一律轉成Rvalue就是了。
而Forward的用途把外面的參數跟語意原封不動的傳進去內部,是Lvalue就是Lvalue,而Rvalue就是Rvalue。
先從比較簡單的Move開始看起,我門打造的第一版Move大概像這樣。
由於Move是個template function,必須進行Template Argument Deduction,此時引進了一條新的規則。
如果傳進來的是Lvalue的話,將會推導成T&,反之如果是Rvalue的話,就會推導成T(根據Reference Collapsing Rules,T和T&&都符合要求,為了解決歧異性,這邊強制要求推導成T)

1
2
3
4
5
template <typename T>
T&& Move(.... value)
{
return static_cast<T&&>(value);
}

這邊先來決定如何傳遞參數,Call by value第一個被否決,接著就是T&T&&的選擇,根據前面的Reference Collapsing Rules,如果是T&的話一律會摺疊成T&,而T&無法繫結至modifiable rvalue,而如果是T&&的話,不管Lvalue跟Rvalue都可以順利繫結。

接著我們來測試第一版的程式碼

1
2
3
4
quark(Move(up));
quark(Move(down));
quark(Move(strange()));
quark(Move(charm()));

印出的結果是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
t: up
T: string&
T&&: string&

t: down
T: const string&
T&&: const string&

t: strange()
T: string
T&&: string&&

t: charm()
T: const string
T&&: const string&&

顯然結果錯了,原因在於實際參數是Lvalue的話,T會被推導成U&,而T&&的結果依然是U&,變成Move傳回去的語意是個Lvalue,因此導致上面的結果。
所以我們要做的,就是把U&或U&&一律轉成U&&,也就是std::remove_refernce存在的理由。改寫我們的程式,先用RemoveRefenece取得Primitive Type, 然後加上&&之後就可以得到正確的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
struct RemoveReference {
typedef T type;
};
template <typename T>
struct RemoveReference<T&> {
typedef T type;
};
template <typename T>
struct RemoveReference<T&&> {
typedef T type;
};
template <typename T>
typename RemoveReference<T>::type&& Move(T&& t)
{
return static_cast<RemoveReference<T>::type&&>(t);
}

gcc的實作就類似於這樣。
接著來討論forward該怎麼做,從上面我們可以知道,我們只能用T&&來傳遞參數。
先來一組helper function來驗證程式的正確性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void inner(std::string &str)
{
cout << "inner(std::string &str)" << endl;
}
void inner(const std::string &str)
{
cout << "inner(const std::string &str)" << endl;
}
void inner(const std::string&& str)
{
cout << "inner(const std::string&& str)" << endl;
}
void inner(std::string&& str)
{
cout << "inner(std::string&& str)" << endl;
}
template <typename T>
void outer(string&& value)
{
return inner(Forward(value));
}
outer(up);
outer(down);
outer(strange());
outer(charm());

而我們第一版的Forward的實作

1
2
3
4
5
template <typename T>
T&& Forward(T& value)
{
return static_cast<T&&>(value);
}

輸出結果則是

1
2
3
4
inner(std::string&& str)
inner(const std::string&& str)
inner(std::string&& str)
inner(const std::string&& str)

從上面知道,value是個左值,所以Type是U&,T被推導成U,T&&被強制轉換成右值,所以輸出的結果如上。避免的方法就是強迫加上template參數。

1
2
3
4
5
template <typename T> 
void outer(string&& value)
{
return inner(Forward<T>(value));
}

重新執行程式,這下結果符合我們的需求了

1
2
3
4
inner(std::string &str)
inner(const std::string &str)
inner(std::string&& str)
inner(const std::string&& str)

這邊還有幾點要說明的
之前的範例,value是左值,而T可能是U&或是U,T&的結果是U&,可以繫結住左值沒有問題。萬一Forward的參數是個右值怎麼辦?

1
inner(Forward<T>(string()));

我們需要另外一個function,解決function resolution的問題。

1
2
3
4
5
template <typename T>
T&& Forward(T&& value)
{
return static_cast<T&&>(value);
}

在傳進來的是個右值時,T被推導成U。T&&正好可以綁定一個Rvalue,解決上面的問題。不過問題又來了,如果是左值的話,T是U&,T&是U&,T&&還是U&,變成兩個function擁有兩個一模一樣的參數型態,Compiler不知道該選哪個。
解決方案就是套用上面Move所引進的RemoveReference,還原成Primitive Type。

1
2
3
4
5
6
7
8
9
10
template <typename T>
T&& Forward(typename RemoveReference<T>::type& value)
{
return static_cast<T&&>(value);
}
template <typename T>
T&& Forward(typename RemoveReference<T>::type&& value)
{
return static_cast<T&&>(value);
}

這樣子有另外一個好處,這個方案禁止了型別推導,不會再有Forward(value)的存在,編譯時期就能指出錯誤。