Effective C++ (5): Implementation

Introduction

本章討論了 變量聲明, 變量轉換(casting), 變量返回, 異常處理, inline, 解決編譯依賴等問題. 信息量較大也涉及到了設計模式的問題, 需仔細咀嚼.

Rule 26: Postpone variable definitions as long as possible

儘量延後程序定義式的出現, 因爲考慮到如果出現 exceptions 的話, 過早定義大量變量會造成不必要的構造和析構函數時間的浪費. 例如:

string encryptPassword(const string& password)
{
    string encrypted;
    if (password.length() < MinimumPasswordLength){
        throw logic_error("Password is too short");
    }
    ...
    return encrypted;
}

如果在中途拋出 logic_error, 就會造成 encrypted 的構造函數析構函數時間的浪費.

Remeber:
儘量延後變量定義式的出現. 這樣做可增加程序的清晰性並改善程序的效率.

Rule 27: Minimize casting

C風格舊式轉型:

(T)expression
T(expression)

C++新式轉型(cast):

const_cast<T>(expression)   // 常量性轉除 cast away the constness
dynamic_cast<T>(expression) // "安全向下轉型", 將一個基類對象指針(或引用)cast到繼承類指針
reinterpret_cast<T>(expression) // 意圖執行低級轉型, 用的較少
static_cast<T>(expression)  //強迫隱式轉換, non-cast轉cast, int轉double等等, 但是不能 const 轉 non-const

新式轉型更好使用, 一是易於識別, 二是目標更加明確化

Remeber:
如果可以儘量避免轉型, 特別在注重效率的代碼中避免 dynamic_casts. 最好使用c++新式轉型

Rule 28: Avoid returning “handles” to object internals

避免返回 handles 指向對象內部成分, 首先, 是爲了提高函數的封裝性.
首先考慮一個例子, 返回矩陣的頂點:

class Rectangle{
public:

Point& upperLeft() const { return pData->ulhc; }

}

對於上面這種情況, 雖然函數聲明爲const, 卻把內部private變量的引用傳出去了, 這個時候 private 變量不再 private, 可以任意修改. 目前較新的編譯器不會通過上述編譯:

main.cpp:30:10: error: binding ‘const Point’ to reference of type ‘Point&’ discards qualifiers

所以說, 有兩種寫法:

  1. 硬是要返回內部變量的 reference (破壞封裝)

    class Rectangle{
    public:
        ...
        Point& upperLeft() { return pData->ulhc; }
        ...
    }
    
  2. 返回值, 保持封裝, 安全性

    class Rectangle{
    public:
        ...
        Point upperLeft() const { return pData->ulhc; }
        ...
    }
    

Remeber:
避免返回 handles 指向對象內部成分(包括成員變量, 函數指針, 迭代器). 這樣可以提高類的封裝性.

Rule 29: Strive for exception-safe code

着一條Rule的信息量較大, 考慮到了異常處理的問題.

首先以例子爲引入, PrettyMenu 是一個用來表現背景團的GUI菜單 class, 該class期望多線程環境, 下面是 PrettyMenuchangeBackground 函數的一個實現:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    lock(&mutex);
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);    // 該構造函數可能發生異常
    ulock(&mutex);
}

上面這段代碼就不是個異常安全函數, 因爲一旦構造函數發生異常, 那麼會產生 資源泄漏數據敗壞. 因爲互斥鎖已經被鎖住, bgImage指向一個刪除的對象, 並且 imageChanges也被累加.

異常安全函數(Exception safety)在異常拋出的時候會:

  • 不泄露任何資源
  • 不允許數據敗壞

異常安全函數除了滿足上面兩個基本條件之外, 還有有三個級別:

  • 基本承諾: 程序內的任何食物仍然保持在有效狀態下
  • 強烈保證: 類似於原子性(Atomic), 如果函數成功就完全成功, 如果失敗, 則恢復到”調用之前狀態”
  • 不拋出(nothrow)保證: 最高承諾, 承諾絕不拋出異常, 作用於內置類型(如 int, 指針等等) 上的所有操作都是 nothrow 的

爲了滿足基本承諾, 上面代碼可以這樣實現:

class PrettyMenu{
    ...
    std::tr1::shared_ptr<Image> bgImage;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock m1(&mutex);
    bgImage.reset(new Image(imgSrc));
    ++imageChanges;
}

這裏使用了第三章提到的 RAII 用對象管理資源的思想, 使用了智能指針保證對象及時釋放. 這樣做, 就提供了基本安全保證.
但是考慮到, 可能在 Image 構造函數拋出異常的時候, 有可能輸入流(input stream) 的讀取記號(read marker)已被移走, 就是說之前的狀態已被改變(可能bgImage.reset已經發生了一部分, 其實按道理是不會發生的, 我們假裝可能吧hhh), 這樣就沒有滿足強烈保證.
要滿足強烈保證, 我們可以通過 copy-and-swap 的方式實現, 原則就是: 爲你打算修改的對象做一份副本, 然後在副本上修改, 在修改確認沒問題之後, 再與原件做 swap, 這樣保證不出錯

要這樣做的話寫法如下:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    using std::swap;
    Lock m1(&mutex);
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));  // 建立 copy

    pNew->bgImage.reset(new Image(imgSrc)); // 修改副本
    ++pNew->imageChanges;
    swap(pImpl, pNew);  // swap
}

Remeber:

  • 異常安全函數( Exception-safe functions ) 即使發生異常也不會泄漏資源或允許任何數據結構敗壞. 這樣的函數分爲三種級別(由低到高): 基本型, 強烈型, 不拋異常型
  • “強烈保證”往往能夠以 copy-and-swap 實現出來, 但”強烈保證”並非對所有函數都可實現或具備實現意義
  • 函數提供的”異常安全保證”通常是它所調用的各個函數的”異常安全保證”最弱者

Rule 30: Understand the ins and outs of inlining.

inline的整體觀念是”對此函數的每一個調用”都以函數本體替換之.
這樣做可能會增加你的目標碼(object code)的大小. inline 造成的代碼膨脹也可能會導致額外的換頁行爲(paging), 降低指令高速緩存裝置的擊中率(instruction cache hit rate).
對 virtual 函數 inline 都是無用的, inline在編譯期進行了替換, 而virtual則是運行期.
不應該對構造函數 和析構函數進行inline, 會使你的代碼膨脹.

Remeber:
大多數 inline 限制在小型和被頻繁調用的函數上, inline對於編譯器來說只是一個建議, 並不是強制執行的. inline也可能會造成代碼膨脹問題

Rule 31: Mnimizing compilation dependencies between files

當某個文件 #include 了另外一個 classes 的定義式, 那麼當其中任何一個文件發生修改的時候, 整個項目都需要重新編譯. 這將會耗費大量時間.

對於這種問題, 我們可以用前置聲明的方式來解決.

#include <string>   // string 是一個 typedef 爲 basic_string<char>
class Date;
class Address;
class Person{
public:
    Person(const std::string &name, const Date &birthday, const Address &addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
    ...
};

如果 Person 裏只含 Address 和 Date 的指針還好, 如果有對象聲明, 那麼編譯器必須在編譯期知道對象的大小:

int main()
{
    Person p(params); // 編譯器需要知道大小
};

對於上面這種情況, 編譯器就無法知道如何分配空間, 會報錯, 對於這種問題, 有兩種解決辦法:

  1. Handle classes

    把 Person 分爲兩個 classes, 一個提供接口, 一個負責實現.

    #include<string>
    #include<memory>
    class PersonImpl;
    class Date;
    class Address;
    class Person{
    public:
        Person(const std::string &name, const Date &birthday, const Address &addr);
        std::string name() const;
        std::string birthDate() const;
        std::string address() const;
        ...
    private:
        std::tr1::shared_ptr<PersonImpl> pImpl;
    };
    

    對象只含一個指針對象, 指向實現類, 這種設計被稱爲 pImpl idiom. 讓接口和實現相分離. 這樣做有個確定就是每個訪問都增加了一個間隔.

  2. Interface classes

    這個思想就是使用c++的 abstract base class(抽象基類) 描述 derived classes的接口. 通常不帶成員變量, 沒有構造函數, 只有一個virtual析構函數以及一組pure virtual函數

    class Person{
    public:
        virtual ~Person();
        virtual std::string name() const = 0;
        virtual std::string birthDate() const = 0;
        virtual std::string address() const = 0;
        ...
    };
    

    然後使用這個類的客戶必須以 Person 的 pointers 和 references 來撰寫應用程序. 這個時候, 還需要使用 factory(工廠)模式:

    class Person{
    public:
        ...
        static std::tr1::shared_ptr<Person> 
            create(const std::string& name, 
                    const Date& birthday,
                    const Address& addr);
        ...
    };
    

    通常會採取上面這種工廠函數 或者 virtual 構造函數, 工廠函數返回指針(最好是智能指針), 指向分配對象. 函數通常被聲明爲 static.
    使用interface classes的成本就是每個函數都是virtual, 增加了個virtual pointer的跳轉時間.

Remeber:

  • 支持”編譯依存性最小化”的一般構想是: 依賴於聲明式, 不要依賴於定義式. 基於此構想的兩個手段是 Handle classes 和 Interface classes
  • 程序庫頭文件應該以”完全且僅有聲明式”( full and declaration-only forms ) 的形式存在.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章