目標
以下代碼能否編譯通過,能否按照期望運行?(點擊展開)
#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),
#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.*mp,a是一個lvalue,mp是資料成員指標; -
p->*mp,mp是資料成員指標; -
逗號運算式,第二個運算元是lvalue;
-
條件運算子
a ? b : c,這里有非常復雜的規則,舉其中一例,當b和c是相同型別的lvalue時; -
字串字面量;
-
顯式轉換為左值參考型別或函式的右值參考型別,
lvalue的性質:
-
與glvalue相同;
-
內置取地址運算子可以作用于lvalue;
-
可修改的lvalue可以出現在內置賦值運算子的左邊;
-
可以用來初始化一個左值參考,
prvalue包括:
-
除字串以外的字面量;
-
函式呼叫或多載運算子運算式,其回傳型別為非參考型別;
-
內置算術運算、邏輯運算、比較運算、取地址運算子運算式;
-
a.m或p->m,m是列舉成員或非靜態成員函式(見下); -
a.*mp或p->*mp,mp是成員函式指標; -
逗號運算式,第二個運算元是rvalue;
-
條件運算子
a ? b : c的部分情況,如b和c是相同型別的prvalue; -
顯式轉換為非參考型別;
-
this指標; -
列舉成員;
-
非型別模板引數,除非它是左值參考型別;
-
lambda運算式,
prvalue的性質:
-
與rvalue相同;
-
不能是多型的;
-
非型別別且非陣列的prvalue沒有cv修飾符,即使寫了也沒有;
-
必須是完全型別;
-
不能是抽象型別或其陣列,
xvalue包括:
-
函式呼叫或多載運算子運算式,其回傳型別為右值參考型別;
-
內置陣列下標運算式
a[n],a是一個陣列rvalue; -
a.m,a是rvalue且m是非參考型別的非靜態資料成員; -
a.*mp,a是一個rvalue,mp是資料成員指標; -
條件運算子
a ? b : c的部分情況,如b和c是相同型別的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, 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>; -
被使用
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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/24317.html
標籤:C++
上一篇:C++基礎之string物件
