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的類型爲intint i;(i)的類型爲int&。這些類型落入若干類別中。在C++98/03中,每一個表達式都是左值或右值。html

左值(lvalue)是指向真實儲存在內存或寄存器中的值的表達式。「l」指的是「left-hand side」,由於在C中只有lvalue才能寫在賦值運算符的左邊。相對地,右值(rvalue,「r」指的是「right-hand side」)只能出如今賦值運算符的右邊。express

有一些例外,如const int i;i雖然是左值但不能出如今賦值運算符的左邊。到了C++,類類型的rvalue卻能夠出如今賦值運算符的左邊,事實上這裏的賦值是對賦值運算符函數的調用,與基本類型的賦值是不一樣的。數組

lvalue能夠理解爲可取地址的值,變量、對指針解引用、對返回類型爲引用類型的函數的調用等,都是lvalue。臨時對象都是rvalue,包括字面量和返回類型爲非引用類型的函數調用等。字符串字面量是個例外,它屬於不可修改的左值。ide

賦值運算符左邊須要一個lvalue,右邊須要一個rvalue,若是給它一個lvalue,該lvalue會被隱式轉換成rvalue。這個過程是理所固然的。函數

動機

C++11引入了右值引用和移動語義。函數返回的右值引用,顧名思義,應該表現得和右值同樣,可是這會破壞不少既有的規則:優化

  • rvalue是匿名的,不必定有存儲空間,但右值引用指向內存中的具體對象,該對象還要被維護着;this

  • rvalue的類型是肯定的,必須是徹底類型,靜態類型與動態類型相同,而右值引用能夠是不徹底類型,也能夠支持多態;spa

  • 非類類型的rvalue沒有cv修飾(constvolatile),但右值引用能夠有,並且修飾符必須保留。指針

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

  • 維持右值引用是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)。

#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;

    int i;
    int&& j = std::move(i);
    static_assert(is_lvalue(j), "");
    static_assert(std::is_rvalue_reference<decltype(j)>::value, "");
  • 函數調用或重載運算符表達式,其返回類型爲左值引用類型,或函數的右值引用類型;

  • 內置賦值、複合賦值、前置自增、前置自減運算符表達式;

  • 內置數組下標表達式a[n]p[n]a爲數組類型,p爲指針類型),a是一個數組lvalue;

  • a.m,除非m是枚舉成員,或非靜態成員函數,或a是rvalue且m是非引用類型的非靜態數據成員;

  • p->m,除非m是枚舉成員,或非靜態成員函數;

  • a.*mpa是一個lvalue,mp是數據成員指針;

  • p->*mpmp是數據成員指針;

  • 逗號表達式,第二個運算數是lvalue;

  • 條件運算符a ? b : c,這裏有很是複雜的規則,舉其中一例,當bc是相同類型的lvalue時;

  • 字符串字面量;

  • 顯式轉換爲左值引用類型或函數的右值引用類型。

lvalue的性質:

  • 與glvalue相同;

  • 內置取地址運算符能夠做用於lvalue;

  • 可修改的lvalue能夠出如今內置賦值運算符的左邊;

  • 能夠用來初始化一個左值引用。

prvalue包括:
  • 除字符串之外的字面量;

  • 函數調用或重載運算符表達式,其返回類型爲非引用類型;

  • 內置算術運算、邏輯運算、比較運算、取地址運算符表達式;

  • a.mp->mm是枚舉成員或非靜態成員函數(見下);

  • a.*mpp->*mpmp是成員函數指針;

  • 逗號表達式,第二個運算數是rvalue;

  • 條件運算符a ? b : c的部分狀況,如bc是相同類型的prvalue;

  • 顯式轉換爲非引用類型;

  • this指針;

  • 枚舉成員;

  • 非類型模板參數,除非它是左值引用類型;

  • lambda表達式。

prvalue的性質:

  • 與rvalue相同;

  • 不能是多態的;

  • 非類類型且非數組的prvalue沒有cv修飾符,即便寫了也沒有;

  • 必須是徹底類型;

  • 不能是抽象類型或其數組。

xvalue包括:
  • 函數調用或重載運算符表達式,其返回類型爲右值引用類型;

  • 內置數組下標表達式a[n]a是一個數組rvalue;

  • a.ma是rvalue且m是非引用類型的非靜態數據成員;

  • a.*mpa是一個rvalue,mp是數據成員指針;

  • 條件運算符a ? b : c的部分狀況,如bc是相同類型的xvalue。

xvalue的性質;

  • 與rvalue相同;

  • 與glvalue相同。

glvalue的性質:

  • 能夠隱式轉換爲prvalue;

  • 能夠是多態的;

  • 能夠是不徹底類型。

rvalue的性質:

  • 內置取地址運算符不能做用於rvalue;

  • 不能出如今內置賦值或複合賦值運算符的左邊;

  • 能夠綁定給const左值引用(見下);

  • 能夠用來初始化右值引用(見下);

  • 若是一個函數有右值引用參數和const左值引用參數兩個重載,傳入一個rvalue時,右值引用的那個重載被調用。

還有一些特殊的分類:

  • 對於非靜態成員函數mf及其指針pmfa.mfp->mfa.*pmfp->*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不是空懸引用,它是指向kconst左值引用。

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, 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, "");
}

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>

  • 被使用typeidsizeof運算符;

  • 在語句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成爲強制性標準,使不能被移動的對象在語義上能夠值傳遞。

參考

相關文章
相關標籤/搜索