一. constexpr和常量表達式
常量表達式(const expression)是指值不會改變並且在編譯過程就能得到計算結果的表達式。顯然,字面值屬於常量表達式,用常量表達式初始化的const對象也是常量表達式。
一個對象(或表達式)是不是常量表達式由它的數據類型和初始值共同決定,例如:
- const int max_files = 20; // max_files是常量表達式
- const int limit = max_files + 1; // limit是常量表達式
- int staff_size = 27; // staff_size不是常量表達式
- const int sz = get_size(); // sz不是常量表達式
盡管staff_size的初始值是個字面值常量,但由於它的數據類型只是一個普通int而非const int,所以它不屬於常量表達式。另一方面,盡管sz本身是一個常量,但它的具體值直到運行時才能獲取到,所以也不是常量表達式。
在一個復雜系統中,很難(幾乎肯定不能)分辨一個初始值到底是不是常量表達式。當然可以定義一個const變量並把它的初始值設為我們認為的某個常量表達式,但在實際使用時,盡管要求如此卻常常發現初始值並非常量表達式的情況。可以這么說,在此種情況下,對象的定義和使用根本就是兩回事兒。
C++11新標准規定,允許將變量聲明為constexpr類型以便由編譯器來驗證變量的值是否是一個常量表達式。聲明為constexpr的變量一定是一個常量,而且必須用常量表達式初始化:
- constexpr int mf = 20; // 20是常量表達式
- constexpr int limit = mf + 1; // mf + 1是常量表達式
- constexpr int sz = size(); // 只有當size是一個constexpr函數時才是一條正確的聲明語句
盡管不能使用普通函數作為constexpr變量的初始值,但是,新標准允許定義一種特殊的constexpr函數。這種函數應該足夠簡單以使得編譯時就可以計算其結果,這樣就能用constexpr函數去初始化constexpr變量了。
一般來說,如果你認定變量是一個常量表達式,那就把它聲明成constexpr類型。
常量表達式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因為這些類型一般比較簡單,值也顯而易見、容易得到,就把它們稱為"字面值類型"(literal type)。
到目前為止接觸過的數據類型中,算術類型、引用和指針都屬於字面值類型。自定義類Sales_item、IO庫、string類型則不屬於字面值類型,也就不能被定義成constexpr。
盡管指針和引用都能定義成constexpr,但它們的初始值卻受到嚴格限制。一個constexpr指針的初始值必須是nullptr或者0,或者是存儲於某個固定地址中的對象。
值得一提的是,函數體內定義的變量一般來說並非存放在固定地址中,因此constexpr指針不能指向這樣的變量。相反的,定義於所有函數體之外的對象其地址固定不變,能用來初始化constexpr指針。同樣,允許函數定義一類有效范圍超出函數本身的變量(即局部靜態變量),這類變量和定義在函數體之外的變量一樣也有固定地址。因此,constexpr引用能綁定到這樣的變量上,constexpr指針也能指向這樣的變量。
指針和constexpr
必須明確一點,在constexpr聲明中如果定義了一個指針,限定符constexpr僅對指針有效,與指針所指的對象無關:
- const int *p = nullptr; // p是一個指向整型常量的指針
- constexpr int *q = nullptr; // q是一個指向整數的常量指針
p和q的類型相差甚遠,p是一個指向常量的指針,而q是一個常量指針,其中的關鍵在於constexpr把它所定義的對象置為了頂層const。
與其他常量指針類似,constexpr指針既可以指向常量也可以指向一個非常量:
- constexpr int *np = nullptr; // np是一個指向整數的常量指針,其值為空
- int j = 0;
- constexpr int i = 42; // i的類型是整型常量
- // i和j都必須定義在函數體之外
- constexpr const int *p = &i; // p 是常量指針,指向整型常量i
- constexpr int *p1 = &j; // p1是常量指針,指向整數j
二. 左值和右值
C++的表達式要不然是右值,要不然就是左值。這兩個名詞是從C語言繼承過來的,原來是為了幫助記憶:左值可以位於賦值語句的左側,右值則不能。
在C++語言中,二者的區別就要復雜很多。
一個左值表達式的求值結果是一個對象或者一個函數,然而以常量對象為代表的某些左值實際上不能作為賦值語句的左側運算對象。此外,雖然某些表達式的求值結果是對象,但它們是右值而非左值。
可以做一個簡單的歸納:當一個對象被用作右值的時候,用的是對象的值(內容),當對象被用作左值的時候,用的是對象的身份(在內存中的位置)。
知乎看到的經典總結:左值右值的形式區分(或者稱語法區分)是能否用取地址&運算符;語義區分(即其本質涵義)在於表達式代表的是持久對象還是臨時對象。
使用關鍵字decltype的時候,左值和右值也有所不同。如果表達式的求值結果是左值,decltype作用於該表達式(不是變量),得到一個引用類型。
如:假定p的類型是int*,因為解引用運算符生成左值,所以decltype(*p)的結果是int&p,另一方面,由於取地址運算符生成右值,所以decltype(&p)的結果是
int**,也就是說是一個指向整型指針的指針。
關於C++中的左值和右值,參考鏈接:C++中的左值和右值,左值右值的一點總結,C++11中的左值和右值,左值、右值與右值引用。
下面是我自己對於左值和右值的一點體會:嚴格來講,左值和右值有一個非常嚴格的評判標准,即是否可以用取地址&運算符。
關於在等號左右兩邊的判斷(左值可以出現在等號左邊或右邊,右值只能出現在右邊)是不完善的,例如有些左值是無法出現在賦值符左邊的,典型的就是常變量對 象:如const int i = 1,變量 i 除了初始化的過程其他情況是不允許再賦值的,但是 i 卻毫無疑問地是左值,可以使用取地址符。
不和對象相關的字面值常量(只有內置類型具有)一般來說是右值,但是有一個例外,就是字符串字面值,可以取地址,是左值,但是也不能位於等號左邊。
相對的是,有時候右值是可以位於等號左邊的,盡管新標准並不提倡這一點。例如,string s1, s2; s1 + s2 = "wow!"。
s1 + s2是右值,但是位於賦值符號的左邊。為了維持向后兼容性,新標准庫仍然允許向右值賦值。
三. 右值引用
為了支持移動操作,新標准引入了一種新的引用類型——右值引用(rvalue referrence)。
所謂右值引用就是必須綁定到右值的引用。我們通過&&而不是&來獲得右值引用。如我們將要看到的,右值引用有一個重要的性質——可以綁定到一個即將銷毀的對象。
因此,我們可以自由地將一個右值引用的資源“移動”到另一個對象中。
類似任何引用,一個右值引用也不過是某一個對象的另一個名字而已。如我們所知,對於左值引用,我們不能將其綁定到需要轉換的表達式、字面常量或是返回右值的表達式。
右值引用有着完全相反的特性:我們可以將一個右值引用綁定到這類表達式上,但不能將一個右值引用直接綁定到一個左值上:
int i = 42; int &r = i; //正確:r引用i int &&rr = i; //錯誤:不能將一個右值綁定到一個左值上 const int &r3 = i * 42 //正確:我們可以將一個const引用綁定到一個左值 int &&rr2 = i * 42 // 正確:將rr2綁定到乘法結果上
返回左值引用的函數,連同賦值、下標、解引用和前置遞增/遞減運算符,都是返回左值的表達式的例子。我們可以將一個左值引用綁定到這類表達式的結果上。
返回非引用類型/右值引用的函數,連同算術、關系、位以及后置遞增/遞減運算符,都生成右值,我們不能將一個左值引用綁定到這類表達式上,但我們可以將一個
const的左值引用或者一個右值引用綁定到這類表達式上。
左值持久,右值短暫
考察左值和右值表達式的列表,兩者相互區別之處就很明顯:左值有持久的狀態,而右值要么是字面常量,要么是在表達式求值過程中創建的臨時對象。
由於右值引用只能綁定到臨時對象,我們得知:
所引用的對象將要被銷毀;該對象沒有其他用戶
這兩個特性意味着:使用右值引用的代碼可以自由地接管所引用的對象的資源。
變量是左值
變量可以看做只有一個運算對象而沒有運算符的表達式,雖然我們很少這樣看待變量。類似其他任何表達式,變量表達式也有左值右值屬性。
變量表達式都是左值。帶來的結果就是,我們不能將一個右值引用綁定到一個右值引用類型的變量上,這有些令人驚訝。
其實有了右值表示臨時對象這一觀察結果,變量是左值這一特性並不令人驚訝。畢竟,變量是持久的,直至離開作用域時才被銷毀。
標准庫move函數
雖然不能將一個右值引用直接綁定到一個左值上,但我們可以顯式地將一個左值轉換為對應的右值引用類型。我們可以通過調用一個名為move的標准庫函數來獲得綁定到
左值上的右值引用,此函數定義在頭文件utility中。
int &&rr3 = std::move(rr1) //ok
move調用告訴編譯器:我們有一個左值,但我們希望像一個右值一樣處理它。我們必須認識到,調用move就意味着承諾:除了對rr1賦值或銷毀它外,我們將不再使用它
在調用move后,我們不能對移后源對象的值做任何假設。