值類別
每個 C++ 表示式(運算子及其運算元、字面量、變數名等)都由兩個獨立的屬性來描述:一個型別和一個值類別。每個表示式都有一個非引用型別,並且每個表示式都精確地屬於三種主要值類別之一:prvalue、xvalue 和 lvalue。
- 計算內建運算子運算元的值(這種 prvalue 沒有結果物件),或者
- 初始化一個物件(這種 prvalue 被認為有一個結果物件)。
- 結果物件可以是一個變數,一個由 new-expression 建立的物件,一個由 臨時實質化 建立的臨時物件,或者它們的成員。請注意,非 void 被丟棄的 表示式具有結果物件(實質化的臨時物件)。此外,每個類和陣列 prvalue 都有一個結果物件,除非它是
decltype
的運算元;
擴充套件內容 |
---|
歷史地,被稱為左值,因為左值可以出現在賦值表示式的左側。通常情況並非總是如此 void foo(); void baz() { int a; // Expression `a` is lvalue a = 4; // OK, could appear on the left-hand side of an assignment expression int &b{a}; // Expression `b` is lvalue b = 5; // OK, could appear on the left-hand side of an assignment expression const int &c{a}; // Expression `c` is lvalue c = 6; // ill-formed, assignment of read-only reference // Expression `foo` is lvalue // address may be taken by built-in address-of operator void (*p)() = &foo; foo = baz; // ill-formed, assignment of function } |
- 一個 rvalue 是一個 prvalue 或一個 xvalue;
擴充套件內容 |
---|
歷史地,被稱為右值,因為右值可以出現在賦值表示式的右側。通常情況並非總是如此 執行此程式碼 #include <iostream> struct S { S() : m{42} {} S(int a) : m{a} {} int m; }; int main() { S s; // Expression `S{}` is prvalue // May appear on the right-hand side of an assignment expression s = S{}; std::cout << s.m << '\n'; // Expression `S{}` is prvalue // Can be used on the left-hand side too std::cout << (S{} = S{7}).m << '\n'; } 輸出 42 7 |
注意:此分類法在過去的 C++ 標準修訂版中經歷了重大變化,詳情請參閱下面的歷史。
擴充套件內容 |
---|
儘管有它們的名稱,這些術語是用來分類表示式,而不是值。 執行此程式碼 #include <type_traits> #include <utility> template <class T> struct is_prvalue : std::true_type {}; template <class T> struct is_prvalue<T&> : std::false_type {}; template <class T> struct is_prvalue<T&&> : std::false_type {}; template <class T> struct is_lvalue : std::false_type {}; template <class T> struct is_lvalue<T&> : std::true_type {}; template <class T> struct is_lvalue<T&&> : std::false_type {}; template <class T> struct is_xvalue : std::false_type {}; template <class T> struct is_xvalue<T&> : std::false_type {}; template <class T> struct is_xvalue<T&&> : std::true_type {}; int main() { int a{42}; int& b{a}; int&& r{std::move(a)}; // Expression `42` is prvalue static_assert(is_prvalue<decltype((42))>::value); // Expression `a` is lvalue static_assert(is_lvalue<decltype((a))>::value); // Expression `b` is lvalue static_assert(is_lvalue<decltype((b))>::value); // Expression `std::move(a)` is xvalue static_assert(is_xvalue<decltype((std::move(a)))>::value); // Type of variable `r` is rvalue reference static_assert(std::is_rvalue_reference<decltype(r)>::value); // Type of variable `b` is lvalue reference static_assert(std::is_lvalue_reference<decltype(b)>::value); // Expression `r` is lvalue static_assert(is_lvalue<decltype((r))>::value); } |
目錄 |
[編輯] 主要類別
[編輯] lvalue
以下表達式是lvalue 表示式
- 變數、函式、非型別模板引數物件(C++20 起)或資料成員的名稱,無論型別如何,例如 std::cin 或 std::endl。即使變數的型別是右值引用,由其名稱組成的表示式也是一個 lvalue 表示式(但請參閱可移動表示式);
擴充套件內容 |
---|
void foo() {} void baz() { // `foo` is lvalue // address may be taken by built-in address-of operator void (*p)() = &foo; } struct foo {}; template <foo a> void baz() { const foo* obj = &a; // `a` is an lvalue, template parameter object } |
- 函式呼叫或過載運算子表示式,其返回型別為左值引用,例如 std::getline(std::cin, str)、std::cout << 1、str1 = str2 或 ++it;
擴充套件內容 |
---|
int& a_ref() { static int a{3}; return a; } void foo() { a_ref() = 5; // `a_ref()` is lvalue, function call whose return type is lvalue reference } |
- a = b、a += b、a %= b 以及所有其他內建賦值和複合賦值表示式;
- ++a 和 --a,內建的前置增量和前置減量表示式;
- *p,內建的解引用表示式;
- a[n] 和 p[n],內建的下標表示式,其中 a[n] 中的一個運算元是陣列左值(C++11 起);
- a.m,物件的成員表示式,除了
m
是成員列舉器或非靜態成員函式,或者 a 是右值且m
是物件型別的非靜態資料成員;
擴充套件內容 |
---|
struct foo { enum bar { m // member enumerator }; }; void baz() { foo a; a.m = 42; // ill-formed, lvalue required as left operand of assignment } struct foo { void m() {} // non-static member function }; void baz() { foo a; // `a.m` is a prvalue, hence the address cannot be taken by built-in // address-of operator void (foo::*p1)() = &a.m; // ill-formed void (foo::*p2)() = &foo::m; // OK: pointer to member function } struct foo { static void m() {} // static member function }; void baz() { foo a; void (*p1)() = &a.m; // `a.m` is an lvalue void (*p2)() = &foo::m; // the same } |
- p->m,內建的指標成員表示式,除了
m
是成員列舉器或非靜態成員函式; - a.*mp,指向物件成員的指標表示式,其中 a 是左值且
mp
是指向資料成員的指標; - p->*mp,內建的指向指標成員的指標表示式,其中
mp
是指向資料成員的指標; - a, b,內建的逗號表示式,其中 b 是左值;
- a ? b : c,某些 b 和 c 的三元條件表示式(例如,當兩者都是相同型別的左值時,但請參閱定義瞭解詳情);
- 字串字面量,例如 "Hello, world!";
- 轉換為左值引用型別的轉換表示式,例如 static_cast<int&>(x) 或 static_cast<void(&)(int)>(x);
- 左值引用型別的非型別模板引數;
template <int& v> void set() { v = 5; // template parameter is lvalue } int a{3}; // static variable, fixed address is known at compile-time void foo() { set<a>(); }
|
(C++11 起) |
屬性
- 同glvalue(下述)。
- 可以透過內建的取地址運算子獲取左值的地址:&++i[1] 和 &std::endl 是有效表示式。
- 可修改的左值可以用作內建賦值和複合賦值運算子的左運算元。
- 左值可用於初始化左值引用;這為表示式標識的物件關聯一個新名稱。
[編輯] prvalue
以下表達式是 prvalue 表示式
- 字面量(字串字面量除外),例如 42、true 或 nullptr;
- 函式呼叫或過載運算子表示式,其返回型別為非引用,例如 str.substr(1, 2)、str1 + str2 或 it++;
- a++ 和 a--,內建的後置增量和後置減量表示式;
- a + b、a % b、a & b、a << b 以及所有其他內建算術表示式;
- a && b、a || b、!a,內建的邏輯表示式;
- a < b、a == b、a >= b 以及所有其他內建比較表示式;
- &a,內建的取地址表示式;
- a.m,物件的成員表示式,其中
m
是成員列舉器或非靜態成員函式[2]; - p->m,內建的指標成員表示式,其中
m
是成員列舉器或非靜態成員函式[2]; - a.*mp,指向物件成員的指標表示式,其中
mp
是指向成員函式的指標[2]; - p->*mp,內建的指向指標成員的指標表示式,其中
mp
是指向成員函式的指標[2]; - a, b,內建的逗號表示式,其中 b 是 prvalue;
- a ? b : c,某些 b 和 c 的三元條件表示式(請參閱定義瞭解詳情);
- 轉換為非引用型別的轉換表示式,例如 static_cast<double>(x)、std::string{} 或 (int)42;
this
指標;- 列舉器;
- 標量型別的非型別模板引數;
template <int v> void foo() { // not an lvalue, `v` is a template parameter of scalar type int const int* a = &v; // ill-formed v = 3; // ill-formed: lvalue required as left operand of assignment }
|
(C++11 起) |
|
(C++20 起) |
屬性
- 同rvalue(下述)。
- prvalue 不能是多型的:它所表示的物件的動態型別始終是表示式的型別。
- 非類非陣列 prvalue 不能是 cv 限定的,除非它為了繫結到 cv 限定型別的引用而被實質化(C++17 起)。(注意:函式呼叫或型別轉換表示式可能導致非類 cv 限定型別的 prvalue,但 cv 限定符通常會立即被剝離。)
- prvalue 不能具有不完整型別(除了型別 void,見下文,或在
decltype
說明符中使用時)。 - prvalue 不能具有抽象類型別或其陣列。
[編輯] xvalue
以下表達式是 xvalue 表示式
- a.m,物件的成員表示式,其中 a 是右值,
m
是物件型別的非靜態資料成員; - a.*mp,指向物件成員的指標表示式,其中 a 是右值,
mp
是指向資料成員的指標; - a, b,內建的逗號表示式,其中 b 是 xvalue;
- a ? b : c,某些 b 和 c 的三元條件表示式(請參閱定義瞭解詳情);
|
(C++11 起) |
|
(C++17 起) |
(C++23 起) |
屬性
- 同 rvalue(下述)。
- 同 glvalue(下述)。
特別地,像所有右值一樣,xvalue 繫結到右值引用,並且像所有 glvalue 一樣,xvalue 可以是多型的,非類 xvalue 可以是cv 限定的。
擴充套件內容 |
---|
執行此程式碼 #include <type_traits> template <class T> struct is_prvalue : std::true_type {}; template <class T> struct is_prvalue<T&> : std::false_type {}; template <class T> struct is_prvalue<T&&> : std::false_type {}; template <class T> struct is_lvalue : std::false_type {}; template <class T> struct is_lvalue<T&> : std::true_type {}; template <class T> struct is_lvalue<T&&> : std::false_type {}; template <class T> struct is_xvalue : std::false_type {}; template <class T> struct is_xvalue<T&> : std::false_type {}; template <class T> struct is_xvalue<T&&> : std::true_type {}; // Example from C++23 standard: 7.2.1 Value category [basic.lval] struct A { int m; }; A&& operator+(A, A); A&& f(); int main() { A a; A&& ar = static_cast<A&&>(a); // Function call with return type rvalue reference is xvalue static_assert(is_xvalue<decltype( (f()) )>::value); // Member of object expression, object is xvalue, `m` is a non-static data member static_assert(is_xvalue<decltype( (f().m) )>::value); // A cast expression to rvalue reference static_assert(is_xvalue<decltype( (static_cast<A&&>(a)) )>::value); // Operator expression, whose return type is rvalue reference to object static_assert(is_xvalue<decltype( (a + a) )>::value); // Expression `ar` is lvalue, `&ar` is valid static_assert(is_lvalue<decltype( (ar) )>::value); [[maybe_unused]] A* ap = &ar; } |
[編輯] 混合類別
[編輯] glvalue
一個 glvalue 表示式 既可以是 lvalue 也可以是 xvalue。
屬性
- glvalue 可以透過左值到右值、陣列到指標或函式到指標的隱式轉換隱式轉換為 prvalue。
- glvalue 可以是多型的:它所標識的物件的動態型別不一定是表示式的靜態型別。
- glvalue 可以具有不完整型別,只要表示式允許。
[編輯] rvalue
一個 rvalue 表示式 既可以是 prvalue 也可以是 xvalue。
屬性
- 右值的地址不能透過內建的取地址運算子獲取:&int()、&i++[3]、&42 和 &std::move(x) 是無效的。
- 右值不能用作內建賦值或複合賦值運算子的左運算元。
- 右值可用於初始化 const 左值引用,在這種情況下,右值標識的臨時物件的生命週期將延長至引用作用域結束。
(C++11 起) |
[編輯] 特殊類別
[編輯] 待決成員函式呼叫
表示式 a.mf 和 p->mf,其中 mf
是非靜態成員函式,以及表示式 a.*pmf 和 p->*pmf,其中 pmf
是指向成員函式的指標,都被歸類為 prvalue 表示式,但它們不能用於初始化引用、作為函式引數,或用於任何目的,除非作為函式呼叫運算子的左運算元,例如 (p->*pmf)(args)。
[編輯] Void 表示式
返回 void 的函式呼叫表示式,轉換為 void 的轉換表示式,以及 throw-expression 都被歸類為 prvalue 表示式,但它們不能用於初始化引用或作為函式引數。它們可以用於被丟棄值的上下文(例如,單獨一行,作為逗號運算子的左運算元等)以及返回 void 的函式中的 return 語句。此外,throw-expression 可以用作條件運算子 ?: 的第二個和第三個運算元。
void 表示式沒有結果物件。 |
(C++17 起) |
[編輯] 位域
指定位域的表示式(例如 a.m,其中 a 是型別 struct A { int m: 3; } 的左值)是一個 glvalue 表示式:它可以作為賦值運算子的左運算元,但不能獲取其地址,並且不能將非 const 左值引用繫結到它。const 左值引用或右值引用可以從位域 glvalue 初始化,但會建立一個位域的臨時副本:它不會直接繫結到位域。
可移動表示式雖然由任何變數名稱組成的表示式是左值表示式,但如果它出現在以下運算元中,則該表示式可能是可移動的 如果一個表示式是可移動的,那麼為了過載決議的目的,它既被視為右值又被視為左值(C++23 前)被視為右值(C++23 起)(因此它可能選擇移動建構函式)。詳見區域性變數和引數的自動移動。 |
(C++11 起) |
[編輯] 歷史
[編輯] CPL
程式語言 CPL 首先引入了表示式的值類別:所有 CPL 表示式都可以以“右值模式”求值,但只有某些型別的表示式在“左值模式”下有意義。當以右值模式求值時,表示式被視為計算值的規則(右側值,或 rvalue)。當以左值模式求值時,表示式實際上給出一個地址(左側值,或 lvalue)。這裡的“左”和“右”代表“賦值的左側”和“賦值的右側”。
[編輯] C
C 程式語言採用了類似的分類法,只是賦值的作用不再重要:C 表示式分為“左值表示式”和其他(函式和非物件值),其中“左值”表示標識物件的表示式,“定位器值”[4]。
[編輯] C++98
2011 年之前的 C++ 遵循 C 模型,但將“rvalue”這個名稱恢復給非左值表示式,將函式變為左值,並添加了規則:引用可以繫結到左值,但只有 const 引用可以繫結到右值。一些非左值 C 表示式在 C++ 中成為左值表示式。
[編輯] C++11
隨著 C++11 中移動語義的引入,值類別被重新定義以描述表示式的兩個獨立屬性[5]
在 C++11 中,那些
- 有身份且不能移動的表示式稱為左值(lvalue)表示式;
- 有身份且可以移動的表示式稱為將亡值(xvalue)表示式;
- 沒有身份但可以移動的表示式稱為純右值(prvalue)表示式;
- 沒有身份且不能移動的表示式不被使用[6]。
具有身份的表示式稱為“glvalue 表示式”(glvalue 代表“廣義左值”)。左值和將亡值都是 glvalue 表示式。
可以從中移動的表示式稱為“rvalue 表示式”。純右值和將亡值都是 rvalue 表示式。
[編輯] C++17
在 C++17 中,在某些情況下複製消除被強制執行,這要求將 prvalue 表示式與其初始化的臨時物件分離,從而形成了我們今天的系統。請注意,與 C++11 的方案相反,prvalue 不再從中移動。
[編輯] 腳註
- ↑ 假設 i 具有內建型別或前置增量運算子過載為返回左值引用。
- ↑ 2.0 2.1 2.2 2.3 特殊右值類別,參見待決成員函式呼叫。
- ↑ 假設 i 具有內建型別或者後置增量運算子未過載為返回左值引用。
- ↑ “C 社群內部對於左值(lvalue)的含義存在分歧,一方認為左值是任何形式的物件定位符,另一方則認為左值在賦值運算子的左側有意義。C89 委員會採納了將左值定義為物件定位符的定義。”—— ANSI C 原理,6.3.2.1/10。
- ↑ Bjarne Stroustrup 的 “新”值術語,2010 年。
- ↑ const prvalues(僅允許用於類型別)和 const xvalues 不繫結到
T&&
過載,但它們繫結到 const T&& 過載,這些過載也被標準歸類為“移動建構函式”和“移動賦值運算子”,滿足此分類目的的“可移動”定義。然而,此類過載不能修改其引數,在實踐中不使用;在沒有它們的情況下,const prvalues 和 const xvalues 繫結到 const T& 過載。
[編輯] 參考
- C++23 標準 (ISO/IEC 14882:2024)
- 7.2.1 值類別 [basic.lval]
- C++20 標準 (ISO/IEC 14882:2020)
- 7.2.1 值類別 [basic.lval]
- C++17 標準 (ISO/IEC 14882:2017)
- 6.10 左值和右值 [basic.lval]
- C++14 標準 (ISO/IEC 14882:2014)
- 3.10 左值和右值 [basic.lval]
- C++11 標準 (ISO/IEC 14882:2011)
- 3.10 左值和右值 [basic.lval]
- C++98 標準 (ISO/IEC 14882:1998)
- 3.10 左值和右值 [basic.lval]
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。
缺陷報告 | 應用於 | 釋出時的行為 | 正確的行為 |
---|---|---|---|
CWG 616 | C++11 | 透過成員訪問和成員訪問 右值指向成員的結果是純右值 |
重新分類為將亡值 |
CWG 1059 | C++11 | 陣列純右值不能被 cv 限定 | 允許 |
CWG 1213 | C++11 | 對陣列右值進行下標操作導致左值 | 重新分類為將亡值 |
[編輯] 另請參見
C 文件 關於 值類別
|
[編輯] 外部連結
1. | C++ 值類別和 decltype 揭秘 — David Mazières, 2021 | |
2. | 經驗性地確定表示式的值類別 — StackOverflow |