天天看點

《C++代碼設計與重用》——2.8 const關鍵字的使用

本節書摘來自異步社群出版社《imperfect c++中文版》一書中的第2章,第2.8節,作者: 【美】martin d.carroll , margaret a.ellis,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

c++代碼設計與重用

2.8 const關鍵字的使用

在程式庫中,const關鍵字的正确使用是很重要的。使用const的最大障礙就是使用者往往未能正确了解const的意義。接下來,我們将讨論如何解釋const,如何使用const,和當我們要改變const的時候,const為什麼不能夠被重新解釋。

我們可以用好幾種方式來解釋關鍵字const。先考慮函數sqrt,它用于計算rational對象的平方根(這裡的rational指2.1節描述有理數的類):

如果我們采用抽象解釋方式,那麼上面這個聲明語句說明,sqrt函數不會使用r去改變r所引用對象的抽象值。如果我們采用位元解釋方式,那麼這個聲明語句說明,sqrt函數不會使用r去改變組成r所引用對象的任何位元。(練習2.10讨論了其他幾種可能的解釋方式。)

相對于抽象const,位元const有一個優點和幾個缺點。如果在任何地方都使用位元const,那麼,對于沒有構造函數和析構函數類型的const對象,我們就可以把它安全地儲存在隻讀記憶體區域(rom,read-only memory)。而且,對一些應用程式而言,把對象儲存在隻讀記憶體區域将會是一個很重要的優化方式。然而,位元const也具有一些缺點:它具有比抽象const更低級别的抽象性。實際上,一個c++程式庫接口的抽象性級别越低,使用這個程式庫就越困難。

而且,使用位元const的程式庫接口暴露了程式庫的實作細節。任何時候的實作細節都被暴露無遺了,而這往往會帶來一些負面效應。例如,假設在我們程式庫的最新版本中,我們決定這樣來優化sqrt函數:使類r的num(分子)和denom(分母)資料成員轉化成最簡形式。如果采用的是位元const的方式,這時對sqrt函數實作的改變将要求我們去除sqrt聲明語句的const關鍵字,而這違反了我們原先使用const的本意。更加遺憾的是,這個改變是源代碼不相容的,是以可能會破壞使用者的代碼。

是以,在程式庫接口和程式庫實作細節上面,程式庫設計者都應該使用抽象const。考慮下面的代碼:

函數reduce把num和denom轉變成最簡形式。因為簡化有理數的表示并沒有改變這個有理數的(抽象)值,是以我們把reduce定義為const函數。

現在考慮下面reduce函數的實作代碼:

在這裡,gcd是一個傳回它的兩個參數的最大公約數的函數。遺憾的是,試圖改變num和denom的語句是非法的,因為c++編譯器根本沒有辦法知道這樣的事實:對num和denom都進行化簡并沒有改變this指針指向對象的值。

我們可以用3種辦法來解決這個問題。首先,我們可以把num和denom聲明為mutable(可變的):

現在,任何試圖對const rational的num和denom成員變量的改變都是合法的(包括試圖改變它們中的一個或者兩個,進而導緻rational對象值的改變的操作,都是合法的)。另一種允許改變num和denom的技術是:隻在我們需要改變的地方,去除const(cast away const):

由于mutable和const_cast是c++相對較新的特性,是以現今有些編譯器并不能實作它們;進而,使用它們的代碼有時就不具備可移植性。另外,一個具有更好可移植性的技術是使用舊式的強制轉型:

試圖使用舊式的cast轉型對const對應的x對象進行轉型,隻有當被轉型的類x具有不少于一個的顯示構造函數時,類x的轉型後的對象才會有x原來定義的行為1。(大多數重要的類都能滿足這個限制條件,即至少有一個顯式構造函數)

第三種避免編譯器産生錯誤的方法是:對我們要改變的對象增加一個間接層(indirection):

那麼,改變num和denom是合法的,即使在rational的成員函數裡面也是如此:

然而,增加一個間接層降低了程式的效率。是以,使用關鍵字mutable将是解決這個問題最好的辦法,除非可移植性是主要的關注因素。

許多c++程式員隻把const當成一個不能捕獲錯誤的讨厭東西,是以,并不是所有的程式員都能充分地使用const。然而,作為c++程式庫的設計者,就沒有這麼多是否使用const的自由了。對于c++程式庫的接口,應該在它應用的每個地方都使用const關鍵字—就是說,使用const的每個地方都可以確定程式庫在此處不會被修改。

如果未能最大限度地使用const,那麼很有可能會給程式庫使用者帶來問題。假設我們想要提供一個庫函數,它帶有兩個參數—1個指向以null結束的字元串指針p和一個指針數組a,a的元素也是以null結束的字元串指針—并且,如果p指向的字元串和a中某個指針元素指向的字元串相等,就傳回真值。我們很可能會像下面這樣定義這個函數:

對編寫下面代碼的使用者而言,這個接口可以順利通過編譯:

然而,下面的代碼卻不能通過編譯:

這個錯誤來源于我們的失誤,因為我們沒有在每個應該使用const的地方都使用const,下面是contains正确的接口:

現在所有的使用者代碼,不管是充分使用const的代碼,還是沒有使用const的代碼,都可以如使用者預期地通過編譯(或者是不通過編譯,但也是使用者預期的)。

對最大限度地使用const的規律存在着一個例外:假設contains函數并沒有改變它的參數a和p的值,我們仍然不應該如下聲明contains函數:

我們在這裡增加的const對使用者沒有産生任何影響,并且,使用者也很少被非引用的(nonreference)參數所影響。而且,如果contains将來的版本由于某種原因需要改變參數a或p的值,那麼這些相應的const也應該被删去,這将破壞相容性。是以,const決不能用于改變非引用(nonreference)參數的值。

有時,程式庫設計者希望能對const作不同于抽象解釋和位元解釋的另一種解釋,然而,大多數對const的解釋并不是類型安全的。考慮下面的類:

noderef是一個指向底層節點的引用;每一個底層節點都隻包含一個int值。函數value傳回節點儲存的這個int值,而函數setvalue則把val的值賦給底層節點這個值。由上面代碼可以看出,value函數和setvalue函數都是const函數,是以這兩個函數都不會改變noderef本身的值(指引用值,類似指針值,而并不是節點值)。

把setvalue成員函數聲明為const函數多少會讓人有些驚訝,是以類noderef的設計者可能希望在應用noderef的時候,可以重新解釋const。例如,設計者可能如下重新解釋所有const的用法:

儲存在底層節點的值并沒有改變。

然而,這個對const的重新解釋是不安全的。下面是在所提出的重新解釋下類noderef的聲明:

在上面的代碼中,setvalue已經不是const函數了,但現在指派運算符變成了const函數!(指派運算符改變了類noderef中的值,但不是儲存在底層節點的值。)然而,這個接口(setvalue函數)包含了一個類型漏洞,請考慮下面的代碼:

函數f在它的聲明中,保證f将不會使用n來改變n所引用節點的值,但在我們對const的重新解釋下,使用m對函數setvalue的調用卻違反了這個保證,并且還可以順利通過編譯,而不産生任何錯誤或警告。而且,讀者可以證明:為了避免這個類型漏洞,最簡單的權宜之計莫過于,删去noderef指派運算符聲明語句中的最後一個const,但這并不能成功,也達不到我們的目的。

是以,我們應該盡量避免對const進行重新解釋。

1譯注:如上例,是指隻有當rational具有公共構造函數時,轉型後的對象let_me_modify才能調用它的成員變量num和denom。

本文僅用于學習和交流目的,不代表異步社群觀點。非商業轉載請注明作譯者、出處,并保留本文的原始連結。

繼續閱讀