1. 程式人生 > 程式設計 >C++98/11/17表示式類別(小結)

C++98/11/17表示式類別(小結)

目標

以下程式碼能否編譯通過,能否按照期望執行?

#include <utility>
#include <type_traits>

namespace cpp98
{

struct A { };
A func() { return A(); }

int main()
{
 int i = 1;
 i = 2;
 // 3 = 4;
 const int j = 5;
 // j = 6;
 i = j;
 func() = A();
 return 0;
}

}

namespace cpp11
{

#define is_lvalue(x) std::is_lvalue_reference<decltype((x))>::value
#define is_prvalue(x) !std::is_reference<decltype((x))>::value
#define is_xvalue(x) std::is_rvalue_reference<decltype((x))>::value
#define is_glvalue(x) (is_lvalue(x) || is_xvalue(x))
#define is_rvalue(x) (is_xvalue(x) || is_prvalue(x))

void func();
int non_reference();
int&& rvalue_reference();
std::pair<int,int> make();

struct Test
{
 int field;
 void member_function()
 {
 static_assert(is_lvalue(field),"");
 static_assert(is_prvalue(this),"");
 }
 enum Enum
 {
 ENUMERATOR,};
};

int main()
{
 int i;
 int&& j = std::move(i);
 Test test;

 static_assert(is_lvalue(i),"");
 static_assert(is_lvalue(j),"");
 static_assert(std::is_rvalue_reference<decltype(j)>::value,"");
 static_assert(is_lvalue(func),"");
 static_assert(is_lvalue(test.field),"");
 static_assert(is_lvalue("hello"),"");

 static_assert(is_prvalue(2),"");
 static_assert(is_prvalue(non_reference()),"");
 static_assert(is_prvalue(Test{3}),"");
 static_assert(is_prvalue(test.ENUMERATOR),"");

 static_assert(is_xvalue(rvalue_reference()),"");
 static_assert(is_xvalue(make().first),"");

 return 0;
}

}

namespace reference
{

int&& rvalue_reference()
{
 int local = 1;
 return std::move(local);
}

const int& const_lvalue_reference(const int& arg)
{
 return arg;
}

int main()
{
 auto&& i = rvalue_reference(); // dangling reference
 auto&& j = const_lvalue_reference(2); // dangling reference
 int k = 3;
 auto&& l = const_lvalue_reference(k);
 return 0;
}

}

namespace auto_decl
{

int non_reference() { return 1; }
int& lvalue_reference() { static int i; return i; }
const int& const_lvalue_reference() { return lvalue_reference(); }
int&& rvalue_reference() { static int i; return std::move(i); }

int main()
{
 auto [s1,s2] = std::pair(2,3);
 auto&& t1 = s1;
 static_assert(!std::is_reference<decltype(s1)>::value);
 static_assert(std::is_lvalue_reference<decltype(t1)>::value);

 int i1 = 4;
 auto i2 = i1;
 decltype(auto) i3 = i1;
 decltype(auto) i4{i1};
 decltype(auto) i5 = (i1);
 static_assert(!std::is_reference<decltype(i2)>::value,"");
 static_assert(!std::is_reference<decltype(i3)>::value,"");
 static_assert(!std::is_reference<decltype(i4)>::value,"");
 static_assert(std::is_lvalue_reference<decltype(i5)>::value,"");

 auto n1 = non_reference();
 decltype(auto) n2 = non_reference();
 auto&& n3 = non_reference();
 static_assert(!std::is_reference<decltype(n1)>::value,"");
 static_assert(!std::is_reference<decltype(n2)>::value,"");
 static_assert(std::is_rvalue_reference<decltype(n3)>::value,"");

 auto l1 = lvalue_reference();
 decltype(auto) l2 = lvalue_reference();
 auto&& l3 = lvalue_reference();
 static_assert(!std::is_reference<decltype(l1)>::value,"");
 static_assert(std::is_lvalue_reference<decltype(l2)>::value,"");
 static_assert(std::is_lvalue_reference<decltype(l3)>::value,"");

 auto c1 = const_lvalue_reference();
 decltype(auto) c2 = const_lvalue_reference();
 auto&& c3 = const_lvalue_reference();
 static_assert(!std::is_reference<decltype(c1)>::value,"");
 static_assert(std::is_lvalue_reference<decltype(c2)>::value,"");
 static_assert(std::is_lvalue_reference<decltype(c3)>::value,"");

 auto r1 = rvalue_reference();
 decltype(auto) r2 = rvalue_reference();
 auto&& r3 = rvalue_reference();
 static_assert(!std::is_reference<decltype(r1)>::value,"");
 static_assert(std::is_rvalue_reference<decltype(r2)>::value,"");
 static_assert(std::is_rvalue_reference<decltype(r3)>::value,"");

 return 0;
}

}

namespace cpp17
{

class NonMoveable
{
public:
 int i = 1;
 NonMoveable(int i) : i(i) { }
 NonMoveable(NonMoveable&&) = delete;
};

NonMoveable make(int i)
{
 return NonMoveable{i};
}

void take(NonMoveable nm)
{
 return static_cast<void>(nm);
}

int main()
{
 auto nm = make(2);
 auto nm2 = NonMoveable{make(3)};
 // take(nm);
 take(make(4));
 take(NonMoveable{make(5)});
 return 0;
}

}

int main()
{
 cpp98::main();
 cpp11::main();
 reference::main();
 auto_decl::main();
 cpp17::main();
}

C++98表示式類別

每個C++表示式都有一個型別:42的型別為int,int i;則(i)的型別為int&。這些型別落入若干類別中。在C++98/03中,每個表示式都是左值或右值。

左值(lvalue)是指向真實儲存在記憶體或暫存器中的值的表示式。“l”指的是“left-hand side”,因為在C中只有lvalue才能寫在賦值運算子的左邊。相對地,右值(rvalue,“r”指的是“right-hand side”)只能出現在賦值運算子的右邊。

有一些例外,如const int i;,i雖然是左值但不能出現在賦值運算子的左邊。到了C++,類型別的rvalue卻可以出現在賦值運算子的左邊,事實上這裡的賦值是對賦值運算子函式的呼叫,與基本型別的賦值是不同的。

lvalue可以理解為可取地址的值,變數、對指標解引用、對返回型別為引用型別的函式的呼叫等,都是lvalue。臨時物件都是rvalue,包括字面量和返回型別為非引用型別的函式呼叫等。字串字面量是個例外,它屬於不可修改的左值。

賦值運算子左邊需要一個lvalue,右邊需要一個rvalue,如果給它一個lvalue,該lvalue會被隱式轉換成rvalue。這個過程是理所當然的。

動機

C++11引入了右值引用和移動語義。函式返回的右值引用,顧名思義,應該表現得和右值一樣,但是這會破壞很多既有的規則:

  • rvalue是匿名的,不一定有儲存空間,但右值引用指向記憶體中的具體物件,該物件還要被維護著;
  • rvalue的型別是確定的,必須是完全型別,靜態型別與動態型別相同,而右值引用可以是不完全型別,也可以支援多型;
  • 非類型別的rvalue沒有cv修飾(const和volatile),但右值引用可以有,而且修飾符必須保留。

這給傳統的lvalue/rvalue二分法帶來了挑戰,C++委員會面臨選擇:

  • 維持右值引用是rvalue,新增一些特殊規則;
  • 把右值引用歸為lvalue,新增一些特殊規則;
  • 細化表示式類別。

上述問題只是冰山一角;歷史選擇了第三種方案。

C++11表示式類別

C++11提出了表示式類別(value category)的概念。雖然名叫“value category”,但類別劃分的是表示式而不是值,所以我從標題開始就把它譯為“表示式類別”。C++標準定義表示式為:

An expression is a sequence of operators and operands that specifies a computation. An expression can result in a value and can cause side effects.

每個表示式都是三種類別之一:左值(lvalue)、消亡值(xvalue)和純右值(prvalue),稱為主類別。還有兩種混合類別:lvalue和xvalue統稱範左值(glvalue),xvalue和prvalue統稱右值(rvalue)。

C++98/11/17表示式類別(小結)

#define is_glvalue(x) (is_lvalue(x) || is_xvalue(x))
#define is_rvalue(x) (is_xvalue(x) || is_prvalue(x))

C++11對這些類別的定義如下:

  • lvalue指定一個函式或一個物件;
  • xvalue(eXpiring vavlue)也指向物件,通常接近其生命週期的終點;一些涉及右值引用的表示式的結果是xvalue;
  • gvalue(generalized lvalue)是一個lvalue或xvalue;
  • rvalue是xvalue、臨時物件或它們的子物件,或者沒有關聯物件的值;
  • prvalue(pure rvalue)是不是xvalue的rvalue。

這種定義不是很清晰。具體來講,lvalue包括:(點選展開)

lvalue的性質:

  • 與glvalue相同;
  • 內建取地址運算子可以作用於lvalue;
  • 可修改的lvalue可以出現在內建賦值運算子的左邊;
  • 可以用來初始化一個左值引用。

prvalue包括:

prvalue的性質:

  • 與rvalue相同;
  • 不能是多型的;
  • 非類型別且非陣列的prvalue沒有cv修飾符,即使寫了也沒有;
  • 必須是完全型別;
  • 不能是抽象型別或其陣列。

xvalue包括:

xvalue的性質;

  • 與rvalue相同;
  • 與glvalue相同。

glvalue的性質:

  • 可以隱式轉換為prvalue;
  • 可以是多型的;
  • 可以是不完全型別。

rvalue的性質:

  • 內建取地址運算子不能作用於rvalue;
  • 不能出現在內建賦值或複合賦值運算子的左邊;
  • 可以繫結給const左值引用(見下);
  • 可以用來初始化右值引用(見下);
  • 如果一個函式有右值引用引數和const左值引用引數兩個過載,傳入一個rvalue時,右值引用的那個過載被呼叫。

還有一些特殊的分類:

  • 對於非靜態成員函式mf及其指標pmf,a.mf、p->mf、a.*pmf和p->*pmf都被歸類為prvalue,但它們不是常規的prvalue,而是pending(即將發生的) member function call,只能用於函式呼叫;
  • 返回void的函式呼叫、向void的型別裝換和throw語句都是void表示式,不能用於初始化引用或函式引數;
  • C++中最小的定址單位是位元組,因此位域不能繫結到非const左值引用上;const左值引用和右值引用可以繫結位域,它們指向的是位域的一個拷貝。

終於把5個類別介紹完了。表示式可以分為lvalue、xvalue和prvalue三類,lvalue和prvalue與C++98中的lvalue和rvalue類似,而xvalue則完全是為右值引用而生,兼有glvalue與rvalue的性質。除了這種三分類法外,表示式還可以分為lvalue和rvalue兩類,它們之間的主要差別在於是否可以取地址;還可以分為glvalue和prvalue兩類,它們之間的主要差別在於是否存在實體,glvalue有實體,因而可以修改原物件,xvalue常被壓榨剩餘價值。

引用繫結

我們稍微岔開一會,來看兩個與表示式分類相關的特性。

引用繫結有以下型別:

  • 左值引用繫結lvalue,cv修飾符只能多不能少;
  • 右值引用可以繫結rvalue,我們通常不給右值引用加cv修飾符;
  • const左值引用可以繫結rvalue。

左值引用繫結lvalue天經地義,沒什麼需要關照的。但rvalue都是臨時物件,繫結給引用就意味著要繼續用它,它的生命週期會受到影響。通常,rvalue的生命週期會延長到繫結引用的宣告週期,但有以下例外:

  • 由return語句返回的臨時物件在return語句結束後即銷燬,這樣的函式總是會返回一個空懸引用(dangling reference);
  • 繫結到初始化列表中的引用的臨時物件的生命週期只延長到建構函式結束——這是個缺陷,在C++14中被修復;
  • 繫結到函式引數的臨時物件的生命週期延長到函式呼叫所在表示式結束,把該引數作為引用返回會得到空懸引用;
  • 繫結到new表示式中的引用的臨時物件的生命週期只延長到包含new的表示式的結束,不會跟著那個物件。

簡而言之,臨時變數的生命週期只能延長一次。

#include <utility>

int&& rvalue_reference()
{
 int local = 1;
 return std::move(local);
}

const int& const_lvalue_reference(const int& arg)
{
 return arg;
}

int main()
{
 auto&& i = rvalue_reference(); // dangling reference
 auto&& j = const_lvalue_reference(2); // dangling reference
 int k = 3;
 auto&& l = const_lvalue_reference(k);
}

rvalue_reference返回一個指向區域性變數的引用,因此i是空懸引用;2繫結到const_lvalue_reference的引數arg上,函式返回後延長的生命週期達到終點,因此j也是懸空引用;k在傳參的過程中根本沒有臨時物件創建出來,所以l不是空懸引用,它是指向k的const左值引用。

auto與decltype

從C++11開始,auto關鍵字用於自動推導型別,用的是模板引數推導的規則:如果是拷貝列表初始化,則對應模板引數為std::initializer_list<T>,否則把auto替換為T。至於詳細的模板引數推導規則,要介紹的話未免喧賓奪主了。

還好,這不是我們的重點。在引出重點之前,我們還得先看decltype。

decltype用於宣告一個型別("declare type"),有兩種語法:

  • decltype(entity);
  • decltype(expression)。

第一種,decltype的引數是沒有括號包裹的識別符號或類成員,則decltype產生該實體的型別;如果是結構化繫結,則產生被引型別。

第二種,decltype的引數是不能匹配第一種的任何表示式,其型別為T,則根據其表示式類別討論:

  • 如果是xvalue,產生T&&——#define is_xvalue(x) std::is_rvalue_reference<decltype((x))>::value;
  • 如果是lvalue,產生T&——#define is_lvalue(x) std::is_lvalue_reference<decltype((x))>::value;
  • 如果是prvalue,產生T——#define is_prvalue(x) !std::is_reference<decltype((x))>::value。

因此,decltype(x)和decltype((x))產生的型別通常是不同的。

對於不帶引用修飾的auto,初始化器的表示式類別會被抹去,為此C++14引入了新語法decltype(auto),產生的型別為decltype(expr),其中expr為初始化器。對於區域性變數,等號右邊加上一對圓括號,可以保留表示式類別。

#include <utility>
#include <type_traits>

int non_reference() { return 1; }
int& lvalue_reference() { static int i; return i; }
const int& const_lvalue_reference() { return lvalue_reference(); }
int&& rvalue_reference() { static int i; return std::move(i); }

int main()
{
 auto [s1,3);
 auto&& t1 = s1;
 static_assert(!std::is_reference<decltype(s1)>::value);
 static_assert(std::is_lvalue_reference<decltype(t1)>::value);

 int i1 = 4;
 auto i2 = i1;
 decltype(auto) i3 = i1;
 decltype(auto) i4{i1};
 decltype(auto) i5 = (i1);
 static_assert(!std::is_reference<decltype(i2)>::value);
 static_assert(!std::is_reference<decltype(i3)>::value);
 static_assert(!std::is_reference<decltype(i4)>::value);
 static_assert(std::is_lvalue_reference<decltype(i5)>::value);

 auto n1 = non_reference();
 decltype(auto) n2 = non_reference();
 auto&& n3 = non_reference();
 static_assert(!std::is_reference<decltype(n1)>::value,"");
}

用auto定義的變數都是int型別,無論函式的返回型別的引用和const修飾;用decltype(auto)定義的變數的型別與函式返回型別相同;auto&&是轉發引用,n3型別為int&&,其餘與decltype(auto)相同。

C++17表示式類別

眾所周知,編譯器常會執行NRVO(named return value optimization),減少一次對函式返回值的移動或拷貝。不過,這屬於C++標準說編譯器可以做的行為,卻沒有保證編譯器會這麼做,因此客戶不能對此作出假設,從而需要提供一個拷貝或移動建構函式,儘管它們可能不會被呼叫。然而,並不是所有情況下都能提供移動建構函式,即使能移動建構函式也未必只是一個指標的交換。總之,我們明知移動建構函式不會被呼叫卻還要硬著頭皮提供一個,這樣做非常形式主義。

所以,C++17規定了拷貝省略,確保在以下情況下,即使拷貝或移動建構函式有可觀察的效果,它們也不會被呼叫,原本要拷貝或移動的物件直接在目標位置構造:

  • 在return表示式中,運算數是忽略cv修飾符以後的返回型別的prvalue;
  • 在初始化中,初始化器是與變數相同型別的prvalue。

值得一提的是,這類行為在C++17中不能算是一種優化,因為不存在用來拷貝或移動的臨時物件。事實上,C++17重新定義了表示式類別:

  • glvalue的求值能確定物件、位域、函式的身份;
  • prvalue的求值初始化物件或位域,或計算運算數的值,由上下文決定;
  • xvalue是表示一個物件或位域的資源能被重用的glvalue;
  • lvalue是不是xvalue的glvalue;
  • rvalue是prvalue或xvalue。

這個定義在功能上與C++11中的相同,但是更清晰地指出了glvalue和prvalue的區別——glvalue產生地址,prvalue執行初始化。

prvalue初始化的物件由上下文決定:在拷貝省略的情形下,prvalue不曾有關聯的物件;其他情形下,prvalue將產生一個臨時物件,這個過程稱為臨時實體化(temporary materialization)。

臨時實體化把一個完全型別的prvalue轉換成xvalue,在以下情形中發生:

  • 把引用繫結到prvalue上;
  • 類prvalue被獲取成員;
  • 陣列prvalue被轉換為指標或下標取元素;
  • prvalue出現在大括號初始化列表中,用於初始化一個std::initializer_list<T>;
  • 被使用typeid或sizeof運算子;
  • 在語句expr;中或被轉換成void,即該表示式的值被丟棄。

或者可以理解為,所有非拷貝省略的場合中的prvalue都會被臨時實體化。

class NonMoveable
{
public:
 int i = 1;
 NonMoveable(int i) : i(i) { }
 NonMoveable(NonMoveable&&) = delete;
};

NonMoveable make(int i)
{
 return NonMoveable{i};
}

void take(NonMoveable nm)
{
 return static_cast<void>(nm);
}

int main()
{
 auto nm = make(2);
 auto nm2 = NonMoveable{make(3)};
 // take(nm);
 take(make(4));
 take(NonMoveable{make(5)});
}

NonMoveable的移動建構函式被宣告為delete,於是拷貝建構函式也被隱式delete。在auto nm = make(2);中,NonMoveable{i}為prvalue,根據拷貝省略的第一條規則,它直接構造為返回值;返回值是NonMoveable的prvalue,與nm型別相同,根據第二條規則,這個prvalue直接在nm的位置上構造;兩部分結合,該宣告式相當於NonMoveable nm{2};。

在MSVC中,這段程式碼不能通過編譯,這是編譯器未能嚴格遵守C++標準的緣故。然而,如果在NonMoveable的移動建構函式中新增輸出語句,程式執行起來也沒有任何輸出,即使在Debug模式下、即使用C++11標準編譯都如此。這也側面反映出拷貝省略的意義。

總結

C++11規定每個表示式都屬於lvalue、xvalue和prvalue三個類別之一,表示式另可分為lvalue和rvalue,或glvalue和prvalue。返回右值引用的函式呼叫是xvalue,右值引用型別的變數是lvalue。

const左值引用和右值引用可以繫結臨時物件,但是臨時物件的宣告週期只能延長一次,返回一個指向區域性變數的右值引用也會導致空懸引用。

識別符號加上一對圓括號成為表示式,decltype用於表示式可以根據其類別產生相應的型別,用decltype(auto)宣告變數可以保留表示式類別。

C++17中prvalue是否有關聯物件由上下文決定,拷貝省略規定了特定情況下物件不經拷貝或移動直接構造,NRVO成為強制性標準,使不能被移動的物件在語義上可以值傳遞。

參考

Value categories - cppreference.com

Value categories - [l,gl,x,r,pr]values

Value Categories in C++17

到此這篇關於C++98/11/17表示式類別的文章就介紹到這了,更多相關C++98/11/17表示式類別內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!