不幸的事,很多成員函數並不能完全通過二進位位常量性的檢驗。特別是,一個經常改變一個指標指向的內容的成員函數。除非這個指標在這個對象中,否則這個函數就是二進位位 const 的,編譯器也不會提出異議。例如,假設我們有一個類似 TextBlock 的類,因為它需要與一個不知 string 為何物的 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[] 返回對象內部資料的引用,這個類還是(不適當地)將它聲明為 const 成員函數(Item 28 將談論一個深入的主題)。先將它放到一邊,看看 operator[] 的實現,它並沒有使用任何手段改變 pText。結果,編譯器愉快地產生了 operator[] 的代碼,因為對所有編譯器而言,它都是二進位位 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"
這裡確實出了問題,你用一個確定的值建立一個常量對象,然後你只是用它調用了 const 成員函數,但是你改變了它的值! 這就引出了邏輯常量性的概念。這一理論的信徒認為:一個 const 成員函數被調用的時候可能會改變對象中的一些二進位位,但是只能用客戶無法感覺到的方法。例如,你的 CTextBlock 類在需要的時候可以儲存文字塊的長度:
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 的實現當然不是二進位位 const 的—— textLength 和 lengthIsValid 都可能會被改變——但是它還是被看作對 const CTextBlock 對象有效。但編譯器不同意,它還是堅持二進位位常量性,怎麼辦呢?
解決方案很簡單:利用以關鍵字 mutable 為表現形式的 C++ 的 const-related 的靈活空間。mutable 將 non-static 資料成員從二進位位常量性的約束中解放出來:
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 成員函數的重複
mutable 對於解決二進位位常量性不太合我的心意的問題是一個不錯的解決方案,但它不能解決全部的 const-related 難題。例如,假設 TextBlock(包括 CTextBlock)中的 operator[] 不僅要返回一個適當的字元的引用,它還要進行邊界檢查,記錄訪問資訊,甚至資料完整性確認,將這些功能加入到 const 和 non-const 的 operator[] 函數中,使它們變成如下這樣的龐然大物:
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;
};
哎呀!你是說重複代碼?還有隨之而來的額外的編譯時間,維護成本以及代碼膨脹等令人頭痛的事情嗎?當然,也可以將邊界檢查等全部代碼轉移到一個單獨的成員函數(當然是私人的)中,並讓兩個版本的 operator[] 來調用它,但是,你還是要重複寫出調用那個函數和返回語句的代碼。
怎樣才能只實現一次 operator[] 功能,又可以使用兩次呢?你可以用一個版本的 operator[] 去調用另一個版本。並通過強制轉型去掉常量性。
作為一個通用規則,強制轉型是一個非常壞的主意,我將投入整個一個 Item 來告訴你不要使用它,但是重複代碼也不是什麼好事。在當前情況下,const 版本的 operator[] 所做的事也正是 non-const 版本所做的,僅有的不同是它有一個 const 傳回型別。在這種情況下,通過轉型去掉傳回型別的常量性是安全的,因為,無論誰調用 non-const operator[],首要條件是有一個 non-const 對象。否則,他不可能調用一個 non-const 函數。所以,即使需要一個強制轉型,讓 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[]
);
}
...
};
正如你看到的,代碼中有兩處強制轉型,而不止一處。我們讓 non-const operator[] 調用 const 版本,但是,如果在 non-const operator[] 的內部,我們僅僅調用了 operator[],那我們將遞迴調用我們自己一百萬次甚至更多。為了避免無限遞迴,我們必須明確指出我們要調用 const operator[],但是沒有直接的辦法能做到這一點,於是我們將 *this 從它本來的類型 TextBlock& 強制轉型到 const TextBlock&。是的,我們使用強制轉型為它加上了 const!所以我們有兩次強制轉型:第一次是為 *this 加上 const(目的是當我們調用 operator[] 時調用的是 const 版本),第二次是從 const operator[] 的傳回值之中去掉 const。
增加 const 的強制轉型是一次安全的轉換(從一個 non-const 對象到一個 const 對象),所以我們用 static_cast 來做。去掉 const 的強制轉型可以用 const_cast 來完成,在這裡我們沒有別的選擇。
在完成其它事情的基礎上,我們在此例中調用了一個操作符,所以,文法看上去有些奇怪。導致其不會贏得選美比賽,但是它通過在 const 版本的 operator[] 之上實現其 non-const 版本而避免重複代碼的方法達到了預期的效果。使用醜陋的文法達到目標是否值得最好由你自己決定,但是這種在一個 const 成員函數的基礎上實現它的 non-const 版本的技術卻非常值得掌握。
更加值得知道的是做這件事的反向方法——通過用 const 版本調用 non-const 版本來避免代碼重複——是你不能做的。記住,一個 const 成員函數承諾不會改變它的對象的邏輯狀態,但是一個 non-const 成員函數不會做這樣的承諾。如果你從一個 const 成員函數調用一個 non-const 成員函數,你將面臨你承諾不會變化的對象被改變的風險。這就是為什麼使用一個 const 成員函數調用一個 non-const 成員函數是錯誤的,對象可能會被改變。實際上,那樣的代碼如果想通過編譯,你必須用一個 const_cast 來去掉 *this 的 const,這樣做是一個顯而易見的麻煩。而反向的調用——就像我在上面的例子中用的——是安全的:一個 non-const 成員函數對一個對象能夠為所欲為,所以調用一個 const 成員函數也沒有任何風險。這就是 static_cast 可以在這裡工作的原因:這裡沒有 const-related 危險。
就像在本文開始我所說的,const 是一件美妙的東西。在指標和迭代器上,在涉及對象的指標,迭代器和引用上,在函數參數和傳回值上,在局部變數上,在成員函數上,const 是一個強有力的盟友。只要可能就用它,你會為你所做的感到高興。
Things to Remember
·將某些東西聲明為 const 有助於編譯器發現使用錯誤。const 能被用於對象的任何範圍,用於函數參數和傳回型別,用於整個成員函數。
·編譯器堅持二進位位常量性,但是你應該用概念上的常量性(conceptual constness)來編程。(此處原文有誤,conceptual constness 為作者在本書第二版中對 logical constness 的稱呼,本文中的稱呼改了,此處卻沒有改。其實此處還是作者新加的部分,卻使用了舊的術語,怪!——譯者)
·當 const 和 non-const 成員函數具有本質上相同的實現的時候,使用 non-const 版本調用 const 版本可以避免重複代碼。