Effective C++筆記: 設計與聲明(一)

 

Item 18: 使接口易於正確使用,不易被誤用

假設一個用來表現日期的class設計構造函數:

class Date {
public:
  Date(int month, int day, int year);
  ...
};

就可能發生以下錯誤:

Date d(30, 3, 1995);     // Oops! Should be "3, 30" , not "30, 3"

第二,他們可能傳遞一個非法的代表月或日的數字:

Date d(2, 20, 1995);     // Oops! Should be "3, 30" , not "2, 20"

我們可以引入簡單的包裝類型來區別日,月和年,並將這些類型用於 Data 的構造函數。

struct Day {            struct Month {                struct Year {
  explicit Day(int d)     explicit Month(int m)         explicit Year(int y)
  :val(d) {}              :val(m) {}                    :val(y){}

  int val;                int val;                      int val;
};                      };                            };

class Date {
public:
 Date(
const Month& m, const Day& d, const Year& y);
 ...
};
Date d(30, 3, 1995);                      // error! wrong types

Date d(Day(30), Month(3), Year(1995));    // error! wrong types

Date d(Month(3), Day(30), Year(1995));    // okay, types are correct

更進一步,限制Month的值只能從112做到這一點的一種方法是用一個枚舉來表現月,但是枚舉不像我們希望的那樣是類型安全(type-safe)的。例如,枚舉能被作爲整數使用(參見 Item 2)。一個安全的解決方案是預先確定合法的 Month 的集合:

class Month {
public:
  static Month Jan() { return Month(1); }   // functions returning all valid
  static Month Feb() { return Month(2); }   // Month values; see below for
  ...                                       // why these are functions, not
  static Month Dec() { return Month(12); }  // objects

  ...                                       // other member functions

private:
  explicit Month(int m);                    // prevent creation of new
                                            // Month values

  ...                                       // month-specific data
};
Date d(
Month::Mar(), Day(30), Year(1995));

 

除此之外,限制類型內什麼事可以做,什麼事不能做。施加限制的一個普通方法就是加上 const。例如,Item 3 解釋了使 operator* 的返回類型具有 const 資格是如何能夠防止客戶對用戶自定義類型犯下這樣的錯誤:

if (a * b = c) ...                    // oops, meant to do a comparison!

基本上,滿足該條款的最好方法是儘量讓你的新類型的表現和內建類型(如ints)保持一致!!

 

提供行爲一致的接口。STL 容器的接口在很大程度上(雖然並不完美)是一致的,而且這使得它們相當易於使用。例如,每一種 STL 容器都有一個名爲 size 的成員函數可以知道容器中有多少對象。與此對比的是 Java,在那裏你對數組使用 length 屬性,對 String 使用 length 方法,而對 List 卻要使用 size 方法,

 

利用tr1::shared_ptr來保證資源的釋放;

 

總結:

好的接口易於正確使用,而難以錯誤使用。你應該在你的所有接口中爲這個特性努力。

使易於正確使用的方法包括在接口和行爲兼容性上與內建類型保持一致。

預防錯誤的方法包括創建新的類型,限定類型的操作,約束對象的值,以及消除客戶的資源管理職責。

tr1::shared_ptr 支持自定義 deleter。這可以防止 cross-DLL 問題,能用於自動解鎖互斥體(參見 Item 14)等。

 

Item 20: pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)

Pass by reference能避免多餘的構造函數和析構函數的開銷;

 

爲什麼要const?將它聲明爲 const 是必要的,否則調用者必然擔心傳入的參數被改變。

 

reference方式傳遞參數也可以避免slicing(對象切割)問題。示例:

class Window {
public:
  ...
  std::string name() const;           // return name of window
  virtual void display() const;       // draw window and contents
};

class WindowWithScrollBars: public Window {
public:
  ...
  virtual void display() const;
};

 

現在,假設你想寫一個函數打印出一個窗口的名字,並隨後顯示這個窗口。以下這個函數的寫法是錯誤的:

void printNameAndDisplay(Window w)         // incorrect! parameter
{                                          // may be sliced!
  std::cout << w.name();
  w.display();
}

考慮當你用一個 WindowWithScrollBars 對象調用這個函數時會發生什麼:

WindowWithScrollBars wwsb;

printNameAndDisplay(wwsb);   // slicing happened

 

繞過切斷問題的方法就是以傳引用給 const 的方式傳遞 w

void printNameAndDisplay(const Window& w)   // fine, parameter won't
{                                           // be sliced
  std::cout << w.name();
  w.display();
}

 

如果你掀開編譯器的蓋頭偷看一下,你會發現用指針實現引用是非常典型的做法,所以以引用傳遞某物實際上通常意味着傳遞一個指針。由此可以得出結論,如果你有一個內建類型的對象(例如,一個 int),以傳值方式傳遞它常常比傳引用方式更高效。那麼,對於內建類型,當你需要在傳值和傳引用給 const 之間做一個選擇時,沒有道理不選擇傳值。同樣的建議也適用於 STL 中的迭代器(iterators)和函數對象(function objects),因爲,作爲慣例,它們就是爲傳值設計的。迭代器(iterators)和函數對象(function objects)的實現有責任保證拷貝的高效並且不受切斷問題的影響。(這是一個規則如何變化,依賴於你使用 C++ 的哪一個部分的實例——參見 Item 1。)

 

總結:

儘量以pass-by-reference-to-const代替pass-by-value, 高效且可以避免切割問題

這條規則並不適用於內建類型,及 STL 中的迭代器和函數對象類。對於它們,傳值通常更合適。

 

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