條款03:盡可能使用const


1. 總結

  • const可用於任何作用域內的對象、函數參數、函數返回值、成員函數自身,將這些內容聲明為const可幫助編譯器偵測出錯誤用法
  • 對於const成員函數,C++編譯器強制要求bitwise constness,但在編寫程序時應該使用"概念上的常量性"
  • const成員函數可以修改被mutable關鍵字修飾的non-static成員變量
  • 當const和non-const成員函數有着實質等價的函數體時,令non-const版本調用const版本可避免代碼重復,但絕對不能反過來調用

2. const對象

關鍵字const可以用於以下對象。

  • 在class外部修飾global或namespace作用域中的常量
  • 修飾區塊作用域(block scope)中被聲明為static的對象
  • 修飾class內部的static和non-static成員變量
  • 面對指針,根據“左數右指”口訣,可以指出指針自身、指針所指數據,或兩者都是(或都不是)const

STL迭代器以指針為根據塑模出來,所以STL迭代器的作用就像個T *指針。

  • 聲明迭代器為const就像聲明指針為const一樣,即聲明一個T *const指針,迭代器本身不可修改,但其指向的數據可以被修改
  • 如果希望迭代器所指向的數據不可修改,即聲明一個const T *指針,則需要使用const_iterator
std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();        //iter相當於T *const指針
*iter = 10;                                                 //沒問題
++iter;                                                     //錯誤,iter不可修改

std::vector<int>::const_iterator const_iter = vec.begin();  //const_iter相當於const T *指針
*const_iter = 10;                                           //錯誤,*const_iter不可修改
++const_iter;                                               //沒問題

3. const函數返回值和函數參數

const最具威力的用法是面對函數聲明時的應用。在一個函數聲明中,const可以和函數參數、函數返回值、函數自身(如果是成員函數)產生關聯。
const用於函數參數只需記住一條原則:除非函數體中需要改動參數,否則就將它們聲明為const
const用於函數返回值,往往可以降低因使用錯誤而造成的意外,同時又不至於放棄安全性和高效性,舉個例子,看下面有理數類operator *的聲明。

class Rational { ... };
const Rational operator * (const Rational &lhs, const Rational &rhs);
Rational a, b, c;

(a * b) = c;    //有意錯誤,對兩個數的乘積進行賦值,就好比1 = 2一樣
if (a * b = c)  //無意錯誤,將==漏寫為=
  • 如果a、b、c都是內置類型,上述代碼直接就是不合法
  • 而對於重載了operator *的class,由於operator =的存在,如果不對返回值使用const,編譯器就不會報錯
  • 然而,一個良好的用戶自定義類型的特征是避免它們無端地與內置類型不兼容(見條款18)
  • 因此,將operator *的返回值聲明為const,就可以讓編譯器檢測出這種錯誤用法

4. const成員函數

const成員函數的重要性

將const用於成員函數的目的,是為了確認該成員函數可作用於const對象身上。const成員函數之所以重要,基於兩個理由。

  • 它們使class接口比較容易被理解,可以很明顯的得知哪些函數可以改動對象而哪些函數不能
  • 它們使操作const對象成為可能

第2條對於編寫高效代碼是個關鍵,因為如條款20所述,改善C++程序效率的一個根本方法是以const引用的方式傳遞對象。
const引用的可能是const對象,而const對象只能調用const成員函數。
所以此技術可行的前提是,有const成員函數可用來處理取得的const對象;否則,就算能將const對象傳進來,也沒有辦法去處理它。

C++有一個重要特性:兩個成員函數如果只是常量性不同,則可以構成重載,即使它們的參數類型、參數個數、參數順序都完全一致。
基於這個特性,再結合上面提到的高效編碼技巧,就可以得出如下所示的接口設計。

class TextBlock
{
private:
    std::string text;
public:
    //用於const對象,由於const對象內容不允許修改,因此返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        return text[postion];
    }

    //用於non-const對象
    char &operator [] (std::size_t postion)
    {
        return text[postion];
    }
};

void print(const TextBlock &text)
{
    std::cout << text[0];
}

TextBlock text;
const TextBlock const_text;

print(text);        //調用char &operator [] ()
print(const_text);  //調用const char &operator [] () const

bitwise constness

C++編譯器要求const成員函數不能更改對象內的任何non-static成員變量,簡單地說就是const成員函數中不能出現對non-static成員變量的賦值操作。
這種要求實質上是不能更改對象內的任何一個bit,因此叫做bitwise constness。
不幸的是,許多const成員函數雖然不完全具備const性質,但卻能通過C++編譯器的bitwise檢驗,更具體地說,就是:

  • "概念上的常量性"來看,一個更改了指針指向數據的成員函數不能算是const成員函數
  • 但如果只有指針隸屬於對象,那么稱此函數為bitwise constness不會引發編譯器異議
class TextBlock
{
private:
    char *pText;
public:
    char &operator [] (std::size_t postion) const
    {
        return pText[postion];
    }
};

const TextBlock text("Hello");  //聲明一個const對象
char *pc = &text[0];            //調用const char &operator []取得一個指針,指向text的數據
*pc = 'J';                      //通過pc指針將text的數據改為了"Jello"

上面這個class將operator []聲明為const成員函數,但卻返回了一個reference指向對象內部數據,這種做法是錯誤的,條款28對此有深刻討論,我們暫時先忽略它。
從編譯器bitwise constness的角度看,上述代碼不存在任何問題,但你終究還是改變了const對象的值,這種情況導出所謂的logical constness。

logical constness

logical constness指的是,const成員函數可以修改它所處理對象內的某些bits,但前提是用戶察覺不到這種修改。
要想在const成員函數中修改non-static成員變量,需要對這些成員變量使用mutable關鍵字,mutable可以去除non-static成員變量的bitwise constness約束。

class CTextBlock
{
private:
    char *pText;
    mutable std::size_t textLength;  //最近一次計算的文本長度
    mutable bool lengthIsValid;      //目前的長度是否有效
public:
    std::size_t length() const;
};

std::size_t CTextBlock::length() const
{
    if (!lengthIsValid)
    {
        textLength = std::strlen(pText);
        lengthIsValid = true;
    }

    return textLength;
}

length()的實現當然不是bitwise constness,因為textLength和lengthIsValid都可能被修改,但這兩個成員變量被修改對於const CTextBlock對象是可以接受的

5. 在const和non-const成員函數中避免重復

現在我們對class TextBlock做一些修改,假設operator []不單只是返回一個reference指向某字符,還執行邊界檢查、日志數據訪問、數據完整性檢驗等工作。

class TextBlock
{
private:
    std::string text;
public:
    //用於const對象,由於const對象內容不允許修改,因此返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        ...         //邊界檢查
        ...         //日志數據訪問
        ...         //數據完整性檢驗
        return text[postion];
    }

    //用於non-const對象
    char &operator [] (std::size_t postion)
    {
        ...         //邊界檢查
        ...         //日志數據訪問
        ...         //數據完整性檢驗
        return text[postion];
    }
};

operator[]的const和non-const版本中的代碼重復,可能會隨着編譯時間、持續維護、代碼膨脹等因素而成為令人頭痛的問題。
將重復代碼封裝到一個private函數中,並分別在兩個函數中調用它,不失為一個解決該問題的好辦法,但依然存在代碼重復,如函數調用、return語句。
真正最好的辦法是:先實現operator []的const版本,然后在non-const版本中調用它。如下示例代碼所示,這種方法有兩個技術要點。

  • 先使用static_cast為*this添加const屬性
  • 接下來調用const版本成員函數,並使用const_cast去除返回值中的const,最后作為non-const函數的返回值返回
class TextBlock
{
private:
    std::string text;
public:
    //用於const對象,由於const對象內容不允許修改,因此返回值也加了const
    const char &operator [] (std::size_t postion) const
    {
        ...         //邊界檢查
        ...         //日志數據訪問
        ...         //數據完整性檢驗
        return text[postion];
    }

    //用於non-const對象
    char &operator [] (std::size_t postion)
    {
        const TextBlock &const_this = static_cast<const TextBlock &>(*this);  //將自身從TextBlock &轉換為const TextBlock &
        return const_cast<char &>(const_this[postion]);                       //調用const版本的operator [],並去除返回值中的const屬性,然后返回
    }
};

注意,千萬不要令const版本調用ono-const版本來避免代碼重復,因為const版本調用non-const版本的唯一方法是去除自身的const屬性,這絕對不是個好事情。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM