Effective C++ (4): Designs and Declaration

Introduction

這一章主要講述瞭如何去設計對象, 接口.

Rule 18: Make Interfaces easy to use correctly and hard to use incorrectly

在設計接口的時候, 應該儘量做到, 如果使用者能夠通過編譯, 那麼就應該能得到他所期望的結果. 如果有任何錯誤, 應該儘量提前至編譯期就能夠報出來.

例如:
class Date{
public:
Date(int month, int day, int year);

}
在使用時, 可能客戶會弄錯 day 和 month 的位置, 產生錯誤, 那麼可以定義 類型系統(type system) 來避免這種問題, 定義類似於 struct Day, struct Month, struct Year, 並且在類型系統中限制變量的範圍等.

在設定資源獲取接口的時候, 通過返回 shared_ptr 能夠阻止一大羣用戶犯下資源泄漏的問題.

std::tr1::shared_ptr<Investment> createInvestment()
{
    std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0), getRidOfInvestment);    // declare null pointer
    retVal = ...;   // point to the right object
    return retVal;
}

如果能夠直接將pInv指向正確的對象會比先指向null, 再指向正確的對象更好. 這樣做最大的好處是能夠放置 “cross-DLL problem“, 就是解決 “跨 DLL 之 new/delete 成對應用” 的問題, 經常我們在一個文件new了卻忘了在另一個文件delete, 使用 shared_ptr 可以避免這一點.

Remeber:

  • 設計接口應該容易被正確使用, 不容易被錯誤使用. 當錯誤使用的時候應該儘可能提前報錯
  • “促進正確使用” 的辦法包括接口的一致性,以及與內置類型的行爲兼容
  • “阻止勿用” 的辦法包括建立新類型, 限制類型上的操作, 束縛對象值, 以及消除客戶的資源管理責任
  • shared_ptr 支持定製型刪除器( custom deleter ), 可防範 cross-DLL problem

Rule 19: Treat class design as type design

在設計一個類之前, 請先考慮如下問題:

  1. 新type的對象應該如何被創建和銷燬? 影響到你的構造函數, 析構函數, 以及內存分配函數和釋放函數.
  2. 對象的初始化和賦值有怎樣的區別? 區分出如何書寫 copy assignment 函數
  3. 對象如果被pass-by-value的時候意味着什麼? 思考如何書寫拷貝構造函數
  4. 什麼是新 type 的”合法值”? 考慮建立值的約束, 異常拋出等
  5. 新type需要配合某個繼承圖系( inheritance graph )嗎? 影響到 virtual 函數
  6. 新type需要怎樣的轉換? 對於彼此有轉換行爲的類型, 需仔細考慮
  7. 什麼樣的操作符對於新type是合理的? 考慮到重載操作符等
  8. 誰會使用新type的成員? 決定成員的 public, private, protected, 也幫助決定哪個 classes 或 functions 是 friends.
  9. 什麼是新type的”未聲明接口” (undeclared interface)? 它對效率, 異常安全以及資源是用提供何種保證?
  10. 你的新type有多麼一般化? 考慮到 template 的問題
  11. 你真的需要一個新type嗎? 如果只是新加個簡單的機能, 可能單純定一個 non-member 函數或 templates 更有效

Remeber:
Class的設計就是type的設計. 在定義一個新type之前, 請確定你已經考慮過了本條款覆蓋的所有討論主題.

Rule 20: Prefer pass-by-reference-to-const to pass-by-value

slicing problem說的是當一個derived對象以by-value方式傳遞並被視爲一個base class對象的時候, base class對象的copy構造函數會被調用, 並且 “造成此對象的行爲像一個 derived class對象”

Remeber:

  • 儘量以 pass-by-reference-to-const 替換 pass-by-value, 前者通常比後者高效, 並且避免切割問題(slicing problem)
  • 以上規則並不適用於內置類型, 以及STL的迭代器和函數對象. 對它們而言, pass-by-value 往往比較合適

Rule 21: Don’t try to return a reference when you must return an object

在該返回值的時候還是需要返回值的. 一般返回有以下幾種可能:

  • 返回值. 承受該有的構造和析構成本
  • 返回指向 heap-allocated 對象的指針. 傳遞指針的方式會快一點.
  • 返回指向 static 變量的 reference. 一般用於單例模式.

Remeber:
絕對不要返回一個pointer或reference指向一個local stack對象, 或返回一個 reference 指向一個 heap-allocated 對象, 或返回 pointer 或 reference 指向一個 local static 對象但是卻不是單例.

Rule 22: Declare data members private

成員變量的封裝性與”成員變量的內容改變時所破壞的代碼數量”成正比. 所以無論是使用protect還是public, 當一個變量移除的時候, 所有使用它的客戶代碼都會被破壞. 而如果全部使用函數來獲取變量, 那麼可以通過別的途徑來實現這個函數.

Remeber:

  • 切記將成員變量聲明爲 private. 這可賦予客戶訪問數據的一致性, 可細微劃分訪問控制, 允諾約束條件獲得保證, 並提供 class 作者以充分的實現彈性.
  • protected 並不比 public 更具封裝性.

Rule 23: Prefer non-member non-friend functions to member functions

舉個例子, 對於瀏覽器:

class WebBrowser{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    ...
}

如果要寫一個函數, 執行以上三個成員函數, 那麼我是寫一個 clearEverything() 作爲成員函數好一些呢? 還是寫一個 non-member, non-friend 函數好一些呢?
封裝性可理解爲, 讓儘量少的函數可以訪問類的數據.
從這種層面上來說, non-member, non-friend 函數會好一些.

Remeber:
non-member non-friend functions 相較於 member functions 更好, 能夠增加類的封裝性, 包裹彈性( packaging flexibility ).

Rule 24: Declare non-member functions when type conversions should apply to all parameters

如果你需要爲某個函數的所有參數進行類型轉換的, 那麼這個函數必須是個 non-member 的.

以有理數與整數相乘爲例子:

class Rational{
public:
Rational(int numerator = 0, int denominator = 1); /// 構造函數不爲 explicit, 允許 int-to-Rational 隱式轉換
int numerator() const;
int denominator() const;

private:

};

如果將operator* 作爲成員函數來寫的話:

class Rational{
public:
    ...
    const Rational operator* (const Rational& rhs) const;
};

那麼在類型轉換的時候會出現問題:

Rational oneHalf(1,2);
Rational result;
result = onHalf*2;  // 沒問題, 會先將 2 隱式轉換爲 Rational, 然後再相乘
result = 2* onHalf; // 報錯, Rational不在int構造函數參數列表中, 不會進行隱式類型轉換.

在這種時候, 需要對operator所有參數均進行類型轉換, 只能通過 non-member function 來實現:

class Rational{
    ...
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.numerator() * rhs.numerator(),
            lhs.denominator() * rhs.denominator());
}

這樣就能解決剛剛類型轉換的問題, 那麼這個函數要不要設置爲friend函數呢? 在不需要使用到class成員變量就能夠達到目的的情況下, 不需要設置爲 friend , 可以提高封裝性.

Remember:
如果你需要爲某個函數的所有參數進行類型轉換的(包括this指針所隱喻參數), 那麼這個函數必須是個 non-member 的.

Rule 25: Consider support for a non-throwing swap

這條rule解釋起來比較麻煩. 我們先要了解 std swap 是如何實現的:

template<typename T>
void swap(T &a, T &b){
    T temp(a);
    a = b;
    b = temp;
}

整個過程包括3次複製, 所以效率在某些情況是很低的. 比如一個類裏面只有一個指針的時候, swap實質上只是交換指針所指的東西, 而通過 std::swap 則還會複製指針所指向的內容, 造成效率低下:

class Widget{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs)
    {
        ...
        *pImpl = *(rhs.pImpl);   //複製了指針指向之物
        ...
    }
private:
    WidgetImpl* pImpl;
}

對於上面這種情況, 就需要製作自己特化的swap了. 遵從以下步驟:

  1. 提供一個 public swap 函數, 並保證不拋出異常.

    class Widget{
    public:
        ...
        void swap(Widget& other)
        {
            using std::swap;
            swap(pImpl, other.pImpl);
        }
        ...
    };
    
  2. 在你的 class 的命名空間內提供一個 non-member swap, 並令它調用上述 swap成員函數.

    namespace WidgetStuff{
        ...
        template<typename T>
        class Widget { ... };
        ...
        template<typename T>
        void swap(Widget<T>& a, Widget<T>& b)
        {
            a.swap(b);
        }
    }
    
  3. 如果你正在編寫一個 class (而非 class template), 爲你的 class 特化 std::swap. 並令它調用你的 swap 成員函數.

    namespace std{
        template<>              // 修訂後的 std::swap 特化版本
        void swap<Widget>( Widget &a, Widget &b)
        {
            a.swap(b);
        }
    }
    

Remeber:

  • 當 std::swap 對你的類型效率不高的時候, 提供一個 public swap 成員函數, 並確保該函數不拋出異常.
  • 然後再實現一個 non-member swap 來調用前者, 對於 classes(而非 templates), 也請特化 std::swap
  • 調用swap的時候, 請先使用 using std::swap, 然後再不帶任何”命名空間資格修飾”調用swap
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章