C/C++左值性精髓
(一) 左值的前世今生
左值(lvalue)是C/C++運算式的屬性。只有針對一個運算式,才能談論其左值性。
左值性由來已久,早在世界上第一個C標準C89出現之前就已經存在了。早期的定義是基於內建賦值運算子的需求的,能作為賦值運算子的左運算元的運算式屬於左值,只能作為右運算元的運算式屬於右值(rvalue),左值、右值中的左右兩字來源於此。
但左值性的早期定義過於粗糙,導致它具有很大的局限性,有些現象甚至無法解釋。例如數組名,數組屬於聚集,不是標量類型,其內容無法作為一個數值表示,而賦值運算子要求運算元為標量類型(結構是特例),將數組作為賦值運算子的左運算元進行賦值操作顯然不合理,因此,數組名在左值早期定義中屬於右值;另一方面,數組作為完整對象,理應可以進行取地址運算,但是,對於資料抽象,取地址運算子要求運算元是左值,這就形成了一個矛盾,這個矛盾用左值性的早期定義是無法解決的。
於是,出現了另一種關於左值性的觀點。這種觀點認為,左值應該指示出一個容器(對象),通俗點說指示出一個“洞”,可以往這個“洞”裡存放東西(數值),當然也可以修改“洞”裡面的東西。這個“洞”被一個運算式指示(designate),這個運算式就稱為左值運算式。在這個定義中,lvalue中的l不再代表left,而是locator;而rvalue中的r也不再代表right,而是read。這兩個改變表達了不同於賦值運算子運算元的意義,即左值是一個對象指示符,而右值是從這個對象中讀取到的值。有些對象是可修改的,也有些對象不可修改,相對應地,存在可修改的左值和不可修改的左值。
兩種關於左值的不同詮釋在C社區中各自流行,甚至導致了C社區中的爭執。到了C89標準制定的時候,C89委員會對這兩種觀點進行了折衷,將locator定義作為左值的定義,而賦值運算子定義作為左值是否可修改的判定依據,C89在rational中談到了這個問題:
6.3.2 Other operands
6.3.2.1 Lvalues, arrays, and function designators
A difference of opinion within the C community centered around the meaning oflvalue, one group considering an lvalue to be any kind of object locator, another group holding that an lvalue is meaningful on the left side of an assigning
operator. The C89 Committee adopted the definition of lvalue as an object locator. The term modifiable lvalue is used for the second of the above concepts.
因此,在C89中,左值的定義是一個指示對象的運算式,該運算式具有物件類型或非void不完整類型,而右值的定義是運算式的值。請看C89的原文:
6.2.2 Other operands .
6.2.2.1 Lvalues and function designators
An lvalue is an expression (with an object type or an incomplete type other than void) that designates an object: ”
What is sometimes called “rvalue” is in this International Standard described as the “value of an expression”.
後來的C99和C++的各種標準版本一直使用這個定義的主要語義,只對某些微小細節進行了修改。C/C++的左值模型相對與左值性的早期定義是一個巨大的進步,它使左值不再局限於某個運算子的運算規則,而是擴充到物件模型,賦予了左值空前豐富的內涵。
根據C標準的規定,一個運算式是否左值與能否作為內建賦值運算子的左運算元無關,而決定於該運算式是否具有對象或非VOID不完整類型,而當我們判斷一個左值是否可修改時,則根據其能否作為內建賦值運算子的左運算元作為依據,可以的就是可修改的左值,不可以的就是不可修改的左值。
C89還規定左值必定指示一個有效對象,這是一個失誤,因為有些運算式,例如:
int *p;
*p = ......
p雖然沒有指向一個有效對象,但這裡的*p顯然仍是一個左值,C89的定義排除了這種情況,不太合理,這個失誤在C99中得到了修正,C99不再規定左值必須指示一個有效對象,但指出如果使用了一個沒有指示有效對象的左值,其行為是未定義的。C99原文:
6.3.2 Other operands
6.3.2.1 Lvalues, arrays, and function designators
An lvalue is an expression with an object type or an incomplete type other thanvoid; if an lvalue does not designate an object when it is evaluated, the behavior is undefined.
在C/C++中,具有函數類型的運算式稱為函數指示符,例如函數名或者對函數指標的解引用。由於C中的左值性反映的是資料抽象而不是操作抽象,因此C中的函數指示符既不是左值也不是右值,這個觀念也在函數到指標的轉換條款中得到體現,函數到指標的轉換條款僅指出轉換結果是一個指標,但沒有指出結果的左值性。到了C++,這種觀念發生了變化,C++認為,既然左值性是運算式的屬性,作為初等運算式的函數指示符卻沒有左值性是沒有道理的,同時鑒於賦予函數指示符左值性並沒有壞處,因此C++中的左值也包括函數指示符。要注意的是,函數左值不包括非靜態成員函數,這是因為非靜態成員函數的指標與普通指標是很不相同的,C++標準出於強調兩者差異的需要,硬性規定非靜態成員函數的左值不能獲得,從而禁止了非靜態成員函數的左值轉換。
C++關於左值性的定義是這樣的: 一個運算式不是左值就是右值,與C完全不一樣。初看起來,這個“定義”似乎非常“草率”,其實不然,具體原因與C/C++的類型劃分方法有關。C/C++中的類型系統可以有多種劃分方法,例如基本類型與複合類型(也叫衍生類別型)、標量類型與聚集類型,還有一種是根據物件模型來分,分為物件類型、不完整類型、函數類型和參考型別(C沒有參考型別),由於C++規定參考型別屬於左值,同時也把函數納入左值範疇,因此C++的左值包括了物件模型劃分方法中的所有類型,再使用類似C那種左值定義方法是多餘的,C++僅需要指出一個運算式不是左值就是右值就行了,但C就不行,因為C存在既不是左值也不是右值的運算式:函數指示符!