# 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(代碼重復)。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射