Item 3: 只要可能就用 const
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
關於 const 的一件美妙的事情是它允許你指定一種 semantic(語義上的)約束:一個特定的 object(對象)不應該被修改。而 compilers(編譯器)將執行這一約束。它允許你通知 compilers(編譯器)和其他程式員,某個值應該保持不變。如果確實如此,你就應該明確地表示出來,因為這樣一來,你就可以謀求 compilers(編譯器)的協助,確保這個值不會被改變。
keyword(關鍵字)const 非常多才多藝。在 classes(類)的外部,你可以將它用於 global(全域)或 namespace(命名空間)範圍的 constants(常量)(參見 Item 2),以及那些在 file(檔案)、function(函數)或 block(模組)scope(範圍)內被聲明為 static(靜態)的對象。在 classes(類)的內部,你可以將它用於 static(靜態)和 non-static(非靜態)data members(資料成員)上。對於 pointers(指標),你可以指定這個 pointer(指標)本身是 const,或者它所指向的資料是 const,或者兩者都是,或者都不是:
char greeting[] = "Hello";
char *p = greeting; // non-const pointer,
// non-const data
const char *p = greeting; // non-const pointer,
// const data
char * const p = greeting; // const pointer,
// non-const data
const char * const p = greeting; // const pointer,
// const data
這樣的文法本身其實並不像表面上那樣反覆無常。如果 const 出現在星號左邊,則指標 pointed to(指向)的內容為 constant(常量);如果 const 出現在星號右邊,則 pointer itself(指標自身)為 constant(常量);如果 const 出現在星號兩邊,則兩者都為 constant(常量)。
當指標指向的內容為 constant(常量)時,一些人將 const 放在類型之前,另一些人將它放在類型之後星號之前。兩者在意義上並沒有區別,所以,如下兩個函數具有相同的 parameter type(參數類型):
void f1(const Widget *pw); // f1 takes a pointer to a
// constant Widget object
void f2(Widget const *pw); // so does f2
因為它們都存在於實際的代碼中,你應該習慣於這兩種形式。
STL iterators(迭代器)以 pointers(指標)為原型,所以一個 iterator 在行為上非常類似於一個 T* pointer(指標)。聲明一個 iterator 為 const 就類似於聲明一個 pointer(指標)為 const(也就是說,聲明一個 T* const pointer(指標)):不能將這個 iterator 指向另外一件不同的東西,但是它所指向的東西本身可以變化。如果你要一個 iterator 指向一個不能變化的東西(也就是一個 const T* pointer(指標)的 STL 對等物),你需要一個 const_iterator:
std::vector<int> vec;
...
const std::vector<int>::iterator iter = // iter acts like a T* const
vec.begin();
*iter = 10; // OK, changes what iter points to
++iter; // error! iter is const
std::vector<int>::const_iterator cIter = // cIter acts like a const T*
vec.begin();
*cIter = 10; // error! *cIter is const
++cIter; // fine, changes cIter
對 const 最強有力的用法來自於它在 function declarations(函式宣告)中的應用。在一個 function declaration(函式宣告)中,const 既可以用在函數的 return value(傳回值)上,也可以用在個別的 parameters(參數)上,對於 member functions(成員函數),還可以用於整個函數。
一個函數返回一個 constant value(常量值),常常可以在不放棄安全和效率的前提下儘可能減少客戶的錯誤造成的影響。例如,考慮在 Item 24 中考察的 rational numbers(有理數)的 operator* 函數的聲明。
class Rational { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs);
很多第一次看到這些的程式員會不以為然。為什麼 operator* 的結果應該是一個 const object(對象)?因為如果它不是,客戶就可以犯下如此暴行:
Rational a, b, c;
...
(a * b) = c; // invoke operator= on the
// result of a*b!
我不知道為什麼一些程式員要為兩個數的乘積賦值,但是我知道很多程式員這樣做也並非不稱職。所有這些可能來自一個簡單的輸入錯誤(要求這個類型能夠隱式轉型到 bool):
if (a * b = c) ... // oops, meant to do a comparison!
如果 a 和 b 是 built-in type(內建類型),這樣的代碼顯而易見是非法的。一個好的 user-defined types(使用者自訂類型)的特點就是要避免與 built-ins(內建類型)毫無理由的不和諧(參見 Item 18),而且對我來說允許給兩個數的乘積賦值看上去正是毫無理由的。將 operator* 的傳回值聲明為 const 就可以避免這一點,這就是我們要這樣做的理由。
關於 const parameters(參數)沒什麼特別新鮮之處——它們的行為就像 local(局部)的 const objects(對象),而且無論何時,只要你能,你就應該這樣使用。除非你需要改變一個 parameter(參數)或 local object(本機物件)的能力,否則,確保將它聲明為 const。它只需要你鍵入六個字元,就能將你從我們剛剛看到的這個惱人的錯誤中拯救出來:“我想鍵入 '==',但我意外地鍵入了 '='”。
const member functions(const 成員函數)
member functions(成員函數)被聲明為 const 的目的是標明這個 member functions(成員函數)可能會被 const objects(對象)調用。因為兩個原因,這樣的 member functions(成員函數)非常重要。首先,它使一個 class(類)的 interface(介面)更容易被理解。知道哪個函數可以改變 object(對象)而哪個不可以是很重要的。第二,它們可以和 const objects(對象)一起工作。因為,書寫高效代碼有一個很重要的方面,就像 Item 20 所解釋的,提升一個 C++ 程式的效能的基本方法就是 pass objects by reference-to-const(以傳 const 引用的方式傳遞一個對象)。這個技術只有在 const member functions(成員函數)和作為操作結果的 const-qualified objects(被 const 修飾的對象)存在時才是可行的。
很多人沒有注意到這樣的事實,即 member functions(成員函數)在只有 constness(常量性)不同時是可以被 overloaded(重載)的,但這是 C++ 的一個重要特性。考慮一個代表文字區塊的類:
class TextBlock {
public:
...
const char& operator[](std::size_t position) const // operator[] for
{ return text[position]; } // const objects
char& operator[](std::size_t position) // operator[] for
{ return text[position]; } // non-const objects
private:
std::string text;
};
TextBlock 的 operator[]s 可能會這樣使用:
TextBlock tb("Hello");
std::cout << tb[0]; // calls non-const
// TextBlock::operator[]
const TextBlock ctb("World");
std::cout << ctb[0]; // calls const TextBlock::operator[]
順便提一下,const objects(對象)在實際程式中最經常出現的是作為這樣一個操作的結果:passed by pointer- or reference-to-const(以傳指標或者引用給 const 的方式傳遞)。上面的 ctb 的例子是人工假造的。下面這個例子更真實一些:
void print(const TextBlock& ctb) // in this function, ctb is const
{
std::cout << ctb[0]; // calls const TextBlock::operator[]
...
}
通過 overloading(重載) operator[],而且給不同的版本不同的傳回型別,你能對 const 和 non-const 的 TextBlocks 做不同的操作:
std::cout << tb[0]; // fine — reading a
// non-const TextBlock
tb[0] = 'x'; // fine — writing a
// non-const TextBlock
std::cout << ctb[0]; // fine — reading a
// const TextBlock
ctb[0] = 'x'; // error! — writing a
// const TextBlock
請注意這裡的錯誤只與被調用的 operator[] 的 return type(傳回型別)有關,而調用 operator[] 本身總是正確的。錯誤出現在企圖為 const char& 賦值的時候,因為它是 const 版本的 operator[] 的 return type(傳回型別)。
再請注意 non-const 版本的 operator[] 的 return type(傳回型別)是 reference to a char(一個 char 的引用)而不是一個 char 本身。如果 operator[] 只是返回一個簡單的 char,下面的語句將無法編譯:
tb[0] = 'x';
因為改變一個返回 built-in type(內建類型)的函數的傳回值總是非法的。即使它合法,C++ returns objects by value(以傳值方式返回對象)這一事實(參見 Item 20)也意味著被改變的是 tb.text[0] 的一個 copy(拷貝),而不是 tb.text[0] 自己,這不會是你想要的行為。
讓我們為哲學留一點時間。看看一個 member function(成員函數)是 const 意味著什嗎?有兩個主要的概念:bitwise constness(二進位位常量性)(也稱為 physical constness(物理常量性))和 logical constness(邏輯常量性)。
bitwise(二進位位)const 派別堅持認為,一個 member function(成員函數),若且唯若它不改變 object(對象)的任何 data members(資料成員)(static(靜態)除外),也就是說如果不改變 object(對象)內的任何 bits(二進位位),則這個 member function(成員函數)就是 const。bitwise constness(二進位位常量性)的一個好處是比較容易監測違例:編譯器只需要尋找對 data members(資料成員)的 assignments(賦值)。實際上,bitwise constness(二進位位常量性)就是 C++ 對 constness(常量性)的定義,一個 const member function(成員函數)不被允許改變調用它的 object(對象)的任何 non-static data members(非待用資料成員)。
不幸的是,很多效果上並不是完全 const 的 member functions(成員函數)通過了 bitwise(二進位位)的檢驗。特別是,一個經常改變某個 pointer(指標)指向的內容的 member function(成員函數)效果上不是 const 的。除非這個 pointer(指標)在這個 object(對象)中,否則這個函數就是 bitwise(二進位位)const 的,編譯器也不會提出異議。例如,假設我們有一個 TextBlock-like class(類似 TextBlock 的類),因為它需要與一個不知 string objects(對象)為何物的 C API 打交道,所以它需要將它的資料存放區為 char* 而不是 string。
class CTextBlock {
public:
...
char& operator[](std::size_t position) const // inappropriate (but bitwise
{ return pText[position]; } // const) declaration of
// operator[]
private:
char *pText;
};
儘管 operator[] 返回 a reference to the object's internal data(一個引向對象內部資料的引用),這個 class(類)還是(不適當地)將它聲明為一個 const member function(成員函數)(Item 28 將談論一個深入的主題)。先將它放到一邊,看看 operator[] 的實現,它並沒有使用任何手段改變 pText。結果,編譯器愉快地產生了 operator[] 的代碼,因為畢竟對所有編譯器而言,它都是 bitwise(二進位位)const 的,但是我們看看會發生什麼:
const CTextBlock cctb("Hello"); // declare constant object
char *pc = &cctb[0]; // call the const operator[] to get a
// pointer to cctb's data
*pc = 'J'; // cctb now has the value "Jello"
這裡確實出了問題,你用一個 particular value(確定值)建立一個 constant object(常量對象),然後你只是用它調用了 const member functions(成員函數),但是你還是改變了它的值!
這就引出了 logical constness(邏輯常量性)的概念。這一理論的信徒認為:一個 const member function(成員函數)可能會改變調用它的 object(對象)中的一些 bits(二進位位),但是只能用客戶無法察覺的方法。例如,你的 CTextBlock class(類)在需要的時候可以儲存文字區塊的長度:
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
std::size_t textLength; // last calculated length of textblock
bool lengthIsValid; // whether length is currently valid
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); // error! can't assign to textLength
lengthIsValid = true; // and lengthIsValid in a const
} // member function
return textLength;
}
length 的實現當然不是 bitwise(二進位位)const 的—— textLength 和 lengthIsValid 都可能會被改變——但是它還是被看作對 const CTextBlock 對象有效。但編譯器不同意,它還是堅持 bitwise constness(二進位位常量性),怎麼辦呢?
解決方案很簡單:利用以 mutable 聞名的 C++ 的 const-related(const 相關)的靈活空間。mutable 將 non-static data members(非待用資料成員)從 bitwise constness(二進位位常量性)的約束中解放出來:
class CTextBlock {
public:
...
std::size_t length() const;
private:
char *pText;
mutable std::size_t textLength; // these data members may
mutable bool lengthIsValid; // always be modified, even in
}; // const member functions
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); // now fine
lengthIsValid = true; // also fine
}
return textLength;
}
避免 const 和 non-const member functions(成員函數)的重複
mutable 對於解決 bitwise-constness-is-not-what-I-had-in-mind(二進位位常量性不太合我的心意)的問題是一個不錯的解決方案,但它不能解決全部的 const-related(const 相關)難題。例如,假設 TextBlock(包括 CTextBlock)中的 operator[] 不僅要返回一個適當的字元的 reference(引用),它還要進行 bounds checking(邊界檢查),logged access information(記錄訪問資訊),甚至 data integrity validation(資料完整性確認),將這些功能都加入到 const 和 non-const 的 operator[] 函數中(不必為我們現在有著非凡長度的 implicitly inline functions(隱含內嵌函式)而煩惱,參見 Item 30),使它們變成如下這樣的龐然大物:
class TextBlock {
public:
...
const char& operator[](std::size_t position) const
{
... // do bounds checking
... // log access data
... // verify data integrity
return text[position];
}
char& operator[](std::size_t position)
{
... // do bounds checking
... // log access data
... // verify data integrity
return text[position];
}
private:
std::string text;
};
哎呀!你是說 code duplication(重複代碼)?還有隨之而來的額外的編譯時間,維護成本以及代碼膨脹等令人頭痛之類的事情嗎?當然,也可以將 bounds checking(邊界檢查)等全部代碼轉移到一個單獨的 member function(成員函數)(自然是 private(私人)的)中,並讓兩個版本的 operator[] 來調用它,但是,你還是要重複寫出調用那個函數和 return 語句的代碼。
你真正要做的是只實現一次 operator[] 的功能,而使用兩次。換句話說,你可以用一個版本的 operator[] 去調用另一個版本。並可以為我們 casting away(通過強制轉型脫掉)constness(常量性)。
作為一個通用規則,casting(強制轉型)是一個非常壞的主意,我會投入整個一個 Item 的篇幅來告訴你不要使用它(Item 27),但是 code duplication(重複代碼)也不是什麼好事。在當前情況下,const 版本的 operator[] 所做的事也正是 non-const 版本所做的,僅有的不同是它有一個 const-qualified return type(被 const 修飾的傳回型別)。在這種情況下,casting away(通過強制轉型脫掉)return value(傳回型別)的 const 是安全的,因為,無論誰調用 non-const operator[],首先要有一個 non-const object(對象)。否則,它不能調用一個 non-const 函數。所以,即使需要一個 cast(強制轉型),讓 non-const operator[] 調用 const 版本也是避免重複代碼的安全方法。代碼如下,你讀了後面的解釋後對它的理解可能會更加清晰:
class TextBlock {
public:
...
const char& operator[](std::size_t position) const // same as before
{
...
...
...
return text[position];
}
char& operator[](std::size_t position) // now just calls const op[]
{
return
const_cast<char&>( // cast away const on
// op[]'s return type;
static_cast<const TextBlock&>(*this) // add const to *this's type;
[position] // call const version of op[]
);
}
...
};
正如你看到的,代碼中有兩處 casts(強制轉型),而不是一處。我們讓 non-const operator[] 調用 const 版本,但是,如果在 non-const operator[] 的內部,我們僅僅是調用 operator[],那我們將遞迴調用我們自己。它會進行一百萬次甚至更多。為了避免 infinite recursion(無限遞迴),我們必須明確指出我們要調用 const operator[],但是沒有直接的辦法能做到這一點,於是我們將 *this 從 TextBlock& 的自然類型強制轉型到 const TextBlock&。是的,我們使用 cast(強制轉型)為它加上了 const!所以我們有兩次 casts(強制轉型):第一次是為 *this 加上 const(以便在我們調用 operator[] 時調用它的 const 版本),第二次是從 const operator[] 的 return value(傳回值)之中去掉 const。
加上 const 的 cast(強制轉型)僅僅是強制施加一次安全的轉換(從一個 non-const object(對象)到一個 const object(對象)),所以我們用一個 static_cast 來做。去掉 const 只能經由 const_cast 來完成,所以在這裡我們沒有別的選擇。(在技術上,我們有。一個 C-style cast(C 風格的強制轉型)也能工作,但是,就像我在 Item 27 中解釋的,這樣的 casts(強制轉型)很少是一個正確的選擇。如果你不熟悉 static_cast 或 const_cast,Item 27 中包含有一個概述。)
在完成其它事情的基礎上,我們在此例中調用了一個 operator(操作符),所以,文法看上去有些奇怪。導致其不會贏得選美比賽,但是它根據 const 版本的 operator[] 實現其 non-const 版本而避免 code duplication(代碼重複)的方法達到了預期的效果。使用醜陋的文法達到目標是否值得最好由你自己決定,但是這種根據 const member function(成員函數)實現它的 non-const 版本的技術卻非常值得掌握。
更加值得掌握的是做這件事的反向方法——通過用 const 版本調用 non-const 版本來避免重複——是你不能做的。記住,一個 const member function(成員函數)承諾絕不會改變它的 object(對象)的邏輯狀態,但是一個 non-const member function(成員函數)不會做這樣的承諾。如果你從一個 const member function(成員函數)調用一個 non-const member function(成員函數),你將面臨你承諾不會變化的 object(對象)被改變的風險。這就是為什麼使用一個 const member function(成員函數)調用一個 non-const member function(成員函數)是錯誤的,object(對象)可能會被改變。實際上,那樣的代碼如果想通過編譯,你必須用一個 const_cast 來去掉 *this 的 const,這是一個顯而易見的麻煩。而反向的調用——就像我在上面用的——是安全的:一個 non-const member function(成員函數)對一個 object(對象)能夠為所欲為,所以調用一個 const member function(成員函數)也沒有任何風險。這就是為什麼 static_cast 在這種情況下可以工作在 *this 上的原因:這裡沒有 const-related 危險。
就像在本 Item 開始我所說的,const 是一件美妙的東西。在 pointers(指標)和 iterators(迭代器)上,在 pointers(指標),iterators(迭代器)和 references(引用)涉及到的 object(對象)上,在 function parameters(函數參數)和 return types(傳回值)上,在 local variables(局部變數)上,在 member functions(成員函數)上,const 是一個強有力的盟友。只要可能就用它,你會為你所做的感到高興。
Things to Remember
- 將某些東西聲明為 const 有助於編譯器發現使用錯誤。const 能被用於任何 scope(範圍)中的 object(對象),用於 function parameters(函數參數)和 return types(傳回型別),用於整個 member functions(成員函數)。
- 編譯器堅持 bitwise constness(二進位位常量性),但是你應該用 conceptual constness(概念上的常量性)來編程。(此處原文有誤,conceptual constness為作者在本書第二版中對 logical constness 的稱呼,本文中的稱呼改了,此處卻沒有改。其實此處還是作者新加的部分,卻使用了舊的術語,怪!——譯者注)
- 當 const 和 non-const member functions(成員函數)具有本質上相同的實現的時候,使用 non-const 版本調用 const 版本可以避免 code duplication(代碼重複)。