Effective Item21 儘可能使用const

使用const的好處在於它允許指定一種語意上的約束——某種對象不能被修改——編譯器具體來實施這種約束。通過const,你可以通知編譯器和其他程序員某個值要保持不變。只要是這種情況,你就要明確地使用const ,因爲這樣做就可以藉助編譯器的幫助確保這種約束不被破壞。

const關鍵字實在是神通廣大。在類的外面,它可以用於全局或名字空間常量(見條款1和47),以及靜態對象(某一文件或程序塊範圍內的局部對象)。在類的內部,它可以用於靜態和非靜態成員(見條款12)。

對指針來說,可以指定指針本身爲const,也可以指定指針所指的數據爲const,或二者同時指定爲const,還有,兩者都不指定爲const:

char *p              = "hello";          // 非const指針,
                                         // 非const數據

const char *p        = "hello";          // 非const指針,
                                         // const數據

char * const p       = "hello";          // const指針,
                                         // 非const數據

const char * const p = "hello";          // const指針,
                                         // const數據

語法並非看起來那麼變化多端。一般來說,你可以在頭腦裏畫一條垂直線穿過指針聲明中的星號(*)位置,如果const出現在線的左邊,指針指向的數據爲常量;如果const出現在線的右邊,指針本身爲常量;如果const在線的兩邊都出現,二者都是常量。

在指針所指爲常量的情況下,有些程序員喜歡把const放在類型名之前,有些程序員則喜歡把const放在類型名之後、星號之前。所以,下面的函數取的是同種參數類型:

class widget { ... };

void f1(const widget *pw);      // f1取的是指向
                                // widget常量對象的指針

void f2(widget const *pw);      // 同f2

因爲兩種表示形式在實際代碼中都存在,所以要使自己對這兩種形式都習慣。

const的一些強大的功能基於它在函數聲明中的應用。在一個函數聲明中,const可以指的是函數的返回值,或某個參數;對於成員函數,還可以指的是整個函數。

讓函數返回一個常量值經常可以在不降低安全性和效率的情況下減少用戶出錯的機率。實際上正如條款29所說明的,對返回值使用const有可能提高一個函數的安全性和效率,否則還會出問題。

例如,看這個在條款19中介紹的有理數的operator*函數的聲明:

const rational operator*(const rational& lhs,
                         const rational& rhs);

很多程序員第一眼看到它會納悶:爲什麼operator*的返回結果是一個const對象?因爲如果不是這樣,用戶就可以做下面這樣的壞事:

rational a, b, c;

...

(a * b) = c;      // 對a*b的結果賦值

我不知道爲什麼有些程序員會想到對兩個數的運算結果直接賦值,但我卻知道:如果a,b和c是固定類型,這樣做顯然是不合法的。一個好的用戶自定義類型的特徵是,它會避免那種沒道理的與固定類型不兼容的行爲。對我來說,對兩個數的運算結果賦值是非常沒道理的。聲明operator*的返回值爲const可以防止這種情況,所以這樣做纔是正確的。

關於const參數沒什麼特別之處要強調——它們的運作和局部const對象一樣。(但,見條款m19,const參數會導致一個臨時對象的產生)然而,如果成員函數爲const,那就是另一回事了。

const成員函數的目的當然是爲了指明哪個成員函數可以在const對象上被調用。但很多人忽視了這樣一個事實:僅在const方面有不同的成員函數可以重載。這是c++的一個重要特性。再次看這個string類:

class string {
public:

  ...

  // 用於非const對象的operator[]
  char& operator[](int position)
  { return data[position]; }

  // 用於const對象的operator[]
  const char& operator[](int position) const
  { return data[position]; }

private:
  char *data;
};

string s1 = "hello";
cout << s1[0];                  // 調用非const
                                // string::operator[]
const string s2 = "world";
cout << s2[0];                  // 調用const
                                // string::operator[]

通過重載operator[]並給不同版本不同的返回值,就可以對const和非const string進行不同的處理:

string s = "hello";              // 非const string對象

cout << s[0];                    // 正確——讀一個
                                 // 非const string

s[0] = 'x';                      // 正確——寫一個
                                 // 非const string

const string cs = "world";       // const string 對象

cout << cs[0];                   // 正確——讀一個
                                 // const string

cs[0] = 'x';                     // 錯誤!——寫一個
                                 // const string

另外注意,這裏的錯誤只和調用operator[]的返回值有關;operator[]調用本身沒問題。 錯誤產生的原因在於企圖對一個const char&賦值,因爲被賦值的對象是const版本的operator[]函數的返回值。

還要注意,非const operator[]的返回類型必須是一個char的引用——char本身則不行。如果operator[]真的返回了一個簡單的char,如下所示的語句就不會通過編譯:

s[0] = 'x';

因爲,修改一個“返回值爲固定類型”的函數的返回值絕對是不合法的。即使合法,由於c++“通過值(而不是引用)來返回對象”(見條款22)的內部機制的原因,s.data[0]的一個拷貝會被修改,而不是s.data[0]自己,這就不是你所想要的結果了。

讓我們停下來看一個基本原理。一個成員函數爲const的確切含義是什麼?有兩種主要的看法:數據意義上的const(bitwise constness)和概念意義上的const(conceptual constness)。

bitwise constness的堅持者認爲,當且僅當成員函數不修改對象的任何數據成員(靜態數據成員除外)時,即不修改對象中任何一個比特(bit)時,這個成員函數纔是const的。bitwise constness最大的好處是可以很容易地檢測到違反bitwise constness規定的事件:編譯器只用去尋找有無對數據成員的賦值就可以了。實際上,bitwise constness正是c++對const問題的定義,const成員函數不被允許修改它所在對象的任何一個數據成員。

不幸的是,很多不遵守bitwise constness定義的成員函數也可以通過bitwise測試。特別是,一個“修改了指針所指向的數據”的成員函數,其行爲顯然違反了bitwise constness定義,但如果對象中僅包含這個指針,這個函數也是bitwise const的,編譯時會通過。這就和我們的直覺有差異:

class string {
public:
  // 構造函數,使data指向一個
  // value所指向的數據的拷貝
  string(const char *value);

  ...

  operator char *() const { return data;}

private:
  char *data;
};

const string s = "hello";      // 聲明常量對象

char *nasty = s;               // 調用 operator char*() const

*nasty = 'm';                  // 修改s.data[0]

cout << s;                     // 輸出"mello"

顯然,在用一個值創建一個常量對象並調用對象的const成員函數時一定有什麼錯誤,對象的值竟然可以修改!(關於這個例子更詳細的討論參見條款29)

這就導致conceptual constness觀點的引入。此觀點的堅持者認爲,一個const成員函數可以修改它所在對象的一些數據(bits) ,但只有在用戶不會發覺的情況下。例如,假設string類想保存對象每次被請求時數據的長度:

class string {
public:
  // 構造函數,使data指向一個
  // value所指向的數據的拷貝
  string(const char *value): lengthisvalid(false) { ... }

  ...

  size_t length() const;

private:
  char *data;

  size_t datalength;           // 最後計算出的
                               // string的長度

  bool lengthisvalid;          // 長度當前
                               // 是否合法
};

size_t string::length() const
{
  if (!lengthisvalid) {
    datalength = strlen(data); // 錯誤!
    lengthisvalid = true;      // 錯誤!
  }

  return datalength;
}

這個length的實現顯然不符合“bitwise const”的定義——datalength 和lengthisvalid都可以修改——但對const string對象來說,似乎它一定要是合法的纔行。但編譯器也不同意, 它們堅持“bitwise constness”,怎麼辦?

解決方案很簡單:利用c++標準組織針對這類情況專門提供的有關const問題的另一個可選方案。此方案使用了關鍵字mutable,當對非靜態數據成員運用mutable時,這些成員的“bitwise constness”限制就被解除:

class string {
public:

  ...    // same as above

private:
  char *data;

  mutable size_t datalength;      // 這些數據成員現在
                                  // 爲mutable;他們可以在
  mutable bool lengthisvalid;     // 任何地方被修改,即使
                                  // 在const成員函數裏
};

size_t string::length() const
{
  if (!lengthisvalid) {
    datalength = strlen(data);    // 現在合法
    lengthisvalid = true;         // 同樣合法
  }

  return datalength;
}

mutable在處理“bitwise-constness限制”問題時是一個很好的方案,但它被加入到c++標準中的時間不長,所以有的編譯器可能還不支持它。如果是這樣,就不得不倒退到c++黑暗的舊時代去,在那兒,生活很簡陋,const有時可能會被拋棄。

類c的一個成員函數中,this指針就好象經過如下的聲明:

c * const this;              // 非const成員函數中

const c * const this;        // const成員函數中

這種情況下(即編譯器不支持mutable的情況下),如果想使那個有問題的string::length版本對const和非const對象都合法,就只有把this的類型從const c * const改成c * const。不能直接這麼做,但可以通過初始化一個局部變量指針,使之指向this所指的同一個對象來間接實現。然後,就可以通過這個局部指針來訪問你想修改的成員:

size_t string::length() const
{
  // 定義一個不指向const對象的
  // 局部版本的this指針
  string * const localthis =
    const_cast<string * const>(this);

  if (!lengthisvalid) {
    localthis->datalength = strlen(data);
    localthis->lengthisvalid = true;
  }

  return datalength;
}

做的不是很漂亮。但爲了完成想要的功能也就只有這麼做。

當然,如果不能保證這個方法一定可行,就不要這麼做:比如,一些老的“消除const”的方法就不行。特別是,如果this所指的對象真的是const,即,在定義時被聲明爲const,那麼,“消除const”就會導致不可確定的後果。所以,如果想在成員函數中通過轉換消除const,就最好先確信你要轉換的對象最初沒有被定義爲const。

還有一種情況下,通過類型轉換消除const會既有用又安全。這就是:將一個const對象傳遞到一個取非const參數的函數中,同時你又知道參數不會在函數內部被修改的情況時。第二個條件很重要,因爲對一個只會被讀的對象(不會被寫)消除const永遠是安全的,即使那個對象最初曾被定義爲const。

例如,已經知道有些庫不正確地聲明瞭象下面這樣的strlen函數:

size_t strlen(char *s);

strlen當然不會去修改s所指的數據——至少我一輩子沒看見過。但因爲有了這個聲明,對一個const char *類型的指針調用這個函數時就會不合法。爲解決這個問題,可以在給strlen傳參數時安全地把這個指針的const強制轉換掉:

const char *klingongreeting = "nuqneh"; // "nuqneh"即"hello"
                                        //
size_t length =
  strlen(const_cast<char*>(klingongreeting));

但不要濫用這個方法。只有在被調用的函數(比如本例中的strlen)不會修改它的參數所指的數據時,才能保證它可以正常工作。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章