本主題介紹並描述 C++ 中存在的各種值類別(及其值的參考):
- glvalue
- lvalue
- xlvalue
- prvalue
- rvalue
你肯定聽過 l值 和 r值。 但你可能不會用這個主題所呈現的那種方式來思考它們。
C++ 中的每個運算式都會產生屬於上述五個類別之一的值。 C++ 語言的某些面向——其功能與規則——需要正確理解這些值類別,並引用它們。 這些面向包括取得值的位址、複製值、移動值,以及將值轉發到另一個函式。 這個主題不會深入探討所有這些面向,但能提供紮實理解的基礎資訊。
本主題的資訊以 Stroustrup 對價值類別的分析為框架,並以同一性與可移動性這兩個獨立性質為框架 [Stroustrup, 2013]。
l值具有恆等式
一個數值具有 身份性意味著什麼? 如果你擁有(或可以取得)某個值的記憶體位址,並且安全使用它,那麼該值就具有同一性。 這樣一來,你不僅能比較價值的內容,還能透過身份來比較或區分它們。
l值具有可識別的身分。 如今,「lvalue」中的「l」是「left」的縮寫(也就是指定運算式左手邊的 left)這件事,已經只具有歷史上的意義。 在 C++ 中,lvalue 可以出現在賦值的左側 或 右側。 因此,「lvalue」中的「l」其實並不能幫助你理解或定義它們是什麼。 你只需要明白,我們所說的l值是一個具有同一性的值。
l值表達式的例子包括:命名變數或常數;或是回傳參考的函式。 非 lvalue 的表達式範例包括:臨時式;或是以值回傳的函數。
int& get_by_ref() { ... }
int get_by_val() { ... }
int main()
{
std::vector<byte> vec{ 99, 98, 97 };
std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.
int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}
雖然說 lvalue 具有同一性這個說法是對的,但 xvalue 也是如此。 我們稍後會詳細說明什麼是 xvalue 。 目前要注意有一個價值類別叫做 glvalue (意指「廣義 lvalue」)。 glvalue 集合是 l值(也稱為 經典 l值)與 xvalue 的超集。 因此,雖然「一個l值具有同一性」為真,但具有同一性的完整集合即為gl值集合,如圖所示。
R值是可移動的;l值不是
但有些數值並非 glvalue。 換句話說,有些值 你無法 取得記憶體位址(或你無法依賴它有效)。 我們在上述程式碼範例中看到了一些這樣的值。
沒有可靠的記憶體位址聽起來是個缺點。 但事實上,這種數值的優點是你可以 移動 它(通常很便宜),而不是複製它(通常很貴)。 變動一個數值意味著它不再在原本的位置。 因此,應避免在它先前所在的位置存取它。 關於 何時以及如何移動 價值的討論,本主題不在討論範圍內。 在這個主題中,我們只需要知道一個可移動的值被稱為 r值 (或 經典r值)。
「rvalue」中的「r」是「right」的縮寫(例如賦值運算式的右側)。 但你也可以在指定操作之外使用 rvalue,以及 rvalue 的參照。 因此,「rvalue」中的「r」並不是重點。 你只需要明白,我們所說的R值是一個可變動的值。
反之,l值則不可移動,如圖所示。 如果一個l值真的移動,那就違背了 lvalue的定義。 而且對於合理預期能繼續存取 lvalue 的程式碼來說,這將是意料之外的問題。
所以你不能移動一個l值。 但有一類 glvalue(具有識別性的事物集合)是你可以移動的——前提是你知道自己在做什麼(包括小心不要在移動後再存取它)——而那就是 xvalue。 我們稍後在本主題中,會再回顧這個想法,當我們全面檢視價值類別的全貌時。
R值參考與參考綁定規則
本節介紹參考r值的語法。 我們得等到另一個主題深入探討前進與前進,但可以說r值參考是解決這些問題的必要環節。 不過,在探討右值參考之前,我們首先需要更清楚地了解 T&——也就是我們先前一直稱為「參考」的東西。 它其實是「l值(非const)參考」,指的是使用者可以寫入的值。
template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.
lvalue 參照可以綁定到 lvalue,但不能綁定到 rvalue。
接著是 lvalue const 參考(T const&),它們指向的是參考的使用者 無法 寫入的物件(例如常數)。
template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.
一個 lvalue const 參照可以繫結至 lvalue 或 rvalue。
對型別 T rvalue 的引用語法寫為 T&&。 rvalue 參考指的是可移動值——即使用後內容不再需要保留的值(例如臨時值)。 因為整個重點在於移動綁定到右值參考的值(因此會修改該值),所以 const 和 volatile 限定符(也稱為 cv 限定符)不適用於右值參考。
template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.
rvalue 參考會綁定到 rvalue。 事實上,就超載解析而言,rvalue 偏好 綁定於 rvalue 參考,而非 lvalue const 參考。 但 rvalue 參考不能綁定到 lvalue,因為如前所述,rvalue 參考指的是假設不需要保留內容的值(例如移動建構子的參數)。
你也可以在需要傳值參數的地方傳遞 rvalue,這會透過複製建構完成(如果該 rvalue 是 xvalue,則會透過移動建構)。
glvalue 具有恆等性;prvalue 則沒有
在這個階段,我們知道什麼才是有身份的。 我們知道什麼是可移動的,什麼是不可移動的。 但我們還沒有為那組沒有識別的值命名。 這個集合稱為 預值(prvalue),或 純r值。
int& get_by_ref() { ... }
int get_by_val() { ... }
int main()
{
int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}
價值類別的完整圖像
接下來就是將上述資訊和插圖整合成一個整體的整體圖景。
glvalue (i)
glvalue(廣義 lvalue)具有身分。 我們將用「i」作為「有身分」的簡寫。
lvalue (i&!m)
l值(一種gl值)具有身份性,但不可移動。 這些通常是可讀寫的值,一般會以參考或 const 參考的方式傳遞;如果複製成本低,則也可以按值傳遞。 lvalue 無法綁定到 rvalue 參考。
xvalue(i&m)
x值(一種glvalue,也是一種r值)具有恆等性,且同樣可移動。 這可能是一個你決定對其進行移動的原本左值,因為複製的代價很高,而你也會小心避免在之後再存取它。 以下是如何將 lvalue 轉換成 xvalue 的方法。
struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.
在上面的程式碼範例中,我們還沒有移動任何東西。 我們只是藉由將 lvalue 轉型為未具名的 rvalue reference,產生出一個 xvalue。 它仍可透過其 lvalue 名稱來識別;但作為 xvalue,它現在已 能夠被移動。 關於搬遷的原因,以及搬遷的實際樣貌,只能留待另一個話題討論。 但如果這樣比較好理解的話,你可以把「xvalue」中的「x」想成「專家專用」。 透過將 lvalue 轉換為 xvalue(別忘了,xvalue 是 rvalue 的一種),該值就能繫結到 rvalue 參考。
這裡還有另外兩個 xvalue 的例子——呼叫回傳未命名 rvalue 參考的函式,以及存取 xvalue 的成員。
struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.
prvalue(!i&m)
pr值(純r值;一種r值)沒有同一性,但可移動。 這些通常是暫時物件,或是呼叫以值傳回的函式所得的結果,或是對任何其他不是 glvalue 的運算式求值所得的結果。
R值(M)
R值是可移動的。 我們用「m」作為「可移動」的簡稱。
rvalue 參考 總是指 rvalue(假設我們不需要保留其內容的值)。
但是,r值參考本身算是r值嗎? 一個未命名的 rvalue 參考(像上面 xvalue 程式碼範例中展示的那些)就是 xvalue,所以,沒錯,它就是 rvalue。 它偏好綁定於 rvalue 參考函數參數,例如移動建構子的參數。 反之(或許反直覺地),如果一個r值參考有名稱,則由該名稱組成的表達式即為l值。 所以 它不能 綁定到 rvalue 參考參數。 但其實很容易做到——只要再把它拋到一個未命名的rvalue參考(xvalue)就行了。
void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
foo(a); // Calls foo(A&).
foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.
!i&!m
那種沒有身份認同且不可移動的價值,是我們還沒討論過的組合。 但我們可以忽略它,因為這個類別在 C++ 語言中並不實用。
參考整合規則
一個表達式中多個相似的引用(例如l值參考到l值參考,或r值參考到r值參考)會互相抵消。
-
A& &折疊為A&。 -
A&& &&折疊為A&&。
一個表達式中的多個不同參考會合併成一個 lvalue 參考。
-
A& &&折疊為A&。 -
A&& &折疊為A&。
轉發參考
最後一節將對比我們已討論過的r值參考與轉 發參考的不同概念。 在「轉發參考」這個詞出現之前,有些人會用「通用參考」這個詞。
void foo(A&& a) { ... }
-
A&&如我們所見,是 Rvalue 參考。 Const 和 volatile 不適用於 rvalue 參考。 -
foo僅接受 A 型的 r值。 - 右值參考(例如
A&&)之所以存在,是為了讓你能撰寫一個針對傳入暫時物件(或其他右值)情況進行最佳化的多載函式。
template <typename _Ty> void bar(_Ty&& ty) { ... }
-
_Ty&&是轉發參考。 視你傳遞給bar的內容而定,型別 _Ty 可以是 const 或 non-const,且這與它是否為 volatile 或 non-volatile 無關。 -
bar接受任何 _Ty 型別的 lvalue 或 rvalue。 - 傳遞 lvalue 會使轉發參考變成
_Ty& &&,並壓縮到 lvalue 參考_Ty&。 - 傳遞 rvalue 會使轉發參考變成 rvalue 參考
_Ty&&。 - 轉發參考(例如
_Ty&&)存在的原因 不是 為了最佳化,而是為了將你傳給它們的引數以透明且有效率的方式原封不動地轉發出去。 你很可能只有在撰寫(或仔細研究)函式庫程式碼時,才會遇到轉發參考——例如,會轉送建構函式引數的工廠函式。
Sources
- [Stroustrup, 2013] B. Stroustrup:C++程式語言,第四版。 艾迪生-衛斯理。 2013.