天天看點

Effect C++ 筆記 【4 Designs and Declarations】4 設計與聲明

4 設計與聲明

條款18:讓接口容易被正确使用,不易被誤用

//這章舉了幾個例子,一個是用新的 struct 限制輸入參數;  一個是智能指針保證 資源釋放。   需要看完 STL 再回頭看。

Tips:   

  1. “容易被正确使用,不容易被誤用”的接口,首先必須考慮客戶可能做出什麼樣的錯誤。  //全面考慮,各種可能得古怪的輸入
  2. 接口一緻性,以及與内置類型行為相容,有助于 “正确使用”。
  3. 任何接口如果要求客戶必須記得做某些事情,就是有 “不正确傾向”
  4. “阻止誤用” 的辦法  :  建立新類型,限制類型操作( const ),束縛對象值( emule ),消除客戶資源管理責任( 智能指針 )
  5. tr1::shared_ptr 支援定制型删除器。 可防範 DLL問題,可被用來自動解除互斥鎖等等。

條款19:設計 class 猶如設計 type

如何設計高效的 classes,你要面對以下提問:

  1. 新 type 的對象如而後被建立和銷毀?   ——  構造函數、析構函數、記憶體配置設定和釋放(new、delete)
  2. 對象的初始化和對象的指派該有什麼樣的差別? —— 構造函數、指派操作符  (别混淆初始化和指派,調款4)
  3. 新 type 的對象如果被pass by value,意味着什麼? —— 拷貝構造函數定義一個type的pass by value 的實作
  4. 什麼是新 type 的“合法值”? —— 成員函數必須進行錯誤檢查工作
  5. 你的新 type 需要配合某個繼承圖系麼? —— 如果你繼承既有的class,就得受父類的限制,特别是“他們的函數是virtual”的影響(條款34、36)。如果其他類繼承你的class,那會影響你所聲明的函數-尤其是析構函數-是否為virtual(條款7)。
  6. 你的新 type 需要什麼樣的轉換? —— 如果希望允許類型 T1 之物被隐式轉換為 T2 之物,就必須在 class T1 内寫一個類型轉換函數(operator T2)或在 class T2 内寫一個 non-explicit-one-argument(可被單一實參調用)的構造函數。如果你隻允許explicit構造函數存在,就得寫出專門負責執行轉換的函數。
  7. 什麼樣的操作符和函數對此 type 而言是合理的? —— 某些該是 member 函數,某些則否 (23、24、46條款)
  8. 什麼樣的标準函數應該被屏蔽? —— 那些正是你必須聲明為 private的
  9. 誰該取用新 type 的成員? —— 這個問題幫助你決定,那個成員為public,那個為protect或private。以及哪個class或function是friend。
  10. 什麼是新 type 的“未聲明接口”? —— 對效率、異常安全性(見條款29)以及資源運用提供何種保證?沒看懂
  11. 你的新 type 有多一般化? —— 或許應該是一個 class template
  12. 真的需要一個新 type 嗎?

條款20:最好以 pass-by-reference-to-const 替換 pass-by-value

傳值調用,由拷貝構造函數完成。

使用傳引用 代替 傳值 兩個作用:

  1. 對于自定義類型,節約資源(剩了拷貝構造,和析構)
  2. 防止 切割問題(slicing problem)    :    基類指針指向一個派生類對象 ,然後這個指針被函數傳值調用,那麼拷貝構造隻複制了該對象的基類部分。

Tips:

  • 盡量 以 傳const引用 代替 傳值。 高效且避免切割問題。
  • 隻對 内置類型,和 STL 的疊代器、函數對象,使用傳值 。

條款21:必須傳回對象時,别妄想傳回其 reference

所謂 reference 隻是個别名,代表某個【既有】對象。

任何時候看到一個reference聲明式,你都應該立刻問自己,它的另一個名稱是什麼?因為他一定是某物的另一個名稱。

Tips:

  1. 絕不要傳回 pointer 或 reference 指向一個 local stack 對象                         // 局部變量銷毀後,指針懸挂了
  2. 或 heap-allocated對象                                                                           //  可能無法正确的 delete 掉這個對象  ,比如 w=x*y*z,operator*傳回引用的話,x*y傳回的引用就無法delete
  3. 或 指向 local static 對象而又必須使用很多這樣的對象                                  // 資源浪費
  4. 簡單辦法,【傳回一個新對象】!

條款22:将成員變量聲明為private

結論很簡單:【成員變量應該是 private】

Tips:

  1. 将成員變量聲明為 private。 好處:
    • 通路資料一緻性: 如果public接口内每樣東西都是函數,客戶就不需要在打算通路class成員時猶豫是否使用小括号,因為樣東西都是函數。
    • 細微劃分通路控制:如果成員變量設為public,每個人都可以讀寫它。但是以函數取得或者設定其值,可以實作各種控制。
    • 為“所有可能得實作”提供彈性:例如,可使成員變量被讀寫時通知其他對象、驗證class限制條件、函數前提和事後狀态、多線程環境下執行同步控制......
  2. protected 并不比 public 更具封裝性:   一旦你将一個成員變量聲明為 public或protected 而客戶開始使用它,就很難再改變那個成員變量涉及的一切。

條款23:甯以 non-member && non-friend 替換 member 函數

考慮封裝性:作為一種粗糙的量測,越多函數可以通路這個資料,那資料的封裝性就越低。

條款22,曾說,成員變量應為private。因為如果它不是,就有無限的函數可以通路他。而 能夠通路private成員變量的函數 隻有class的 menber函數加上 friend函數 而已。

是以,“非成員非友元”函數比“成員函數”有更大的封裝性。(注意是'非成員且非友元'。 友元函數和成員函數的通路權利是相同的)

C++,比較自然地做法是:   讓這種 為對象提供便利的函數 成為一個non-member函數且位于 其服務的類 的同一個namespace内。

要知道,namespace和classes 不同,前者可以跨越多個源碼檔案而後者不能 。  

這正是C++标準庫的組織方式。 标準庫不是擁有單一的、整體的、龐大的 <C++StandardLibrary>頭檔案并在其中内含std命名空間裡的每個東西,而是有數十個頭檔案,每個頭檔案聲明std的某些機能。 但這種切割方式并不适用于class成員函數,因為一個class必須整體定義,不能分割為片段。

将所有便利函數放在多個頭檔案内,但隸屬同一個命名空間,意味着客戶可以輕松擴充這一組便利函數。 需要做的就是添加更多 非成員非友元函數 到此命名空間。

條款24: 弱所有參數皆需類型轉換,請為此采用 non-member 函數

隐式轉換 發生在 函數參數清單中。 對于member函數,隐式轉換發生在 “被調用的成員函數所隸屬的那個對象”--即this對象--的那個參數表。(這個主要是對operater說的,因為它們容易被重載。是以需要厘清是member的還是non-member,接受的是兩個參數lhs,rhs  還是  隻有其中之一)

【重要】:  member函數的反面 是 non-member, 而不是 friend 。 與某class有關的函數如果不該成為member,不是就一定是friend。non-member函數也完全可以通過class的public接口完成一定功能 。

條款25:考慮寫出一個不抛異常的 swap 函數

典型的标準程式庫提供的 swap 寫法就是 一個中間temp變量拷貝構造和copy assignment操作:

namespace std{

template<typename T>

void swap( T& a, T& b)

{

T temp(a); //調用拷貝構造函數

a=b; // 拷貝指派操作符

b=temp;

}

}

但是,複制中,常見就是“以指針指向一個對象,内含真正資料”那種類型。 是以為“pimple手法”(pointer to implementation)。   這樣寫Widget class,看起來想這樣:

class WidgetImpl {

public:

...

private:

int a, b, c; // 可能有許多資料

std::vector<double> v; //意味着複制很長時間

...

};

class Widget {

public :

Widget ( const Widget& rhs) // 拷貝構造

Widget& operater=(const Widget& rhs)

{

......

*pImpl = *( rhs.pImpl);

......

}

......

private:

WidgetImpl* pImpl

調換兩個 Widget 對象值,我們需要做的 就是調換兩個 pImpl 指針, 但預設的swap算法不知道這點。 

我們希望告訴 std::swap  : 當 Widget 被置換時, 該做的是置換其内部 pImpl 指針。

一個做法是: 将 std::swap 針對 Widget 特化 !   

下面是基本構想,但目前這個形式無法通過編譯:

namespace std {

template <> // 表示一個全特化版本

void swap<Widget> ( Widget& a, Widget& b) // 表示這一特化版本是針對T是 Widget 設計的

{

swap (a.pImpl, b.pImpl); // 隻置換指針就好

}

}

【"template <>" 表示它是 std::swap 的一個全特化(total template specialization)版本,函數名稱之後的“<Widget>”表示這一特化版本系針對“T是Widget”而設計。】

【通常,我們不允許改變 std 命名空間内的任何東西 , 但是 可以為 标準template 制造特化版本 。】

這個函數無法編譯,因為它企圖通路 a和b 的private 成員 pImpl。  我們可以将這個特化聲明為 friend, 但這裡

我們令 Widget 聲明一個名為 swap 的 public 成員函數。 然後将 std::swap特化, 令它調用該成員函數:

class Widget {

public :

...

void swap ( Widget& other)

{

using std::swap ; // 稍後解釋

swap (pImpl, other.pImpl);

}

...

};

namespace std {

template<> //修訂的std::swap 特化版本

void swap<Widget> ( Widget& a, Widget& b)

{

a.swap(b); // 調用Widget類的 swap成員函數

}

}

這種做法與 STL 容器有一緻性 , 【 所有 STL 容器 也都提供 有 public swap 成員函數 和 std::swap 特化版本(用 以調用前者)。】   

假設Widget 和 WidgetImpl 都是 class templates 而非 class。那麼,在Widget内放個swap成員函數沒問題, 但在特化 std::swap 時遇上亂流,我們想寫成這樣:

namespace std {

template<typename T>

void swap< Widget<T> >(Widget& a, Widget b) // 不合法!!!

{ a. swap(b) ;}

}

看起來合理,但不合法。 我們企圖偏特化一個 function template 。【 但C++隻允許對 class template 偏特化 】

當你打算偏特化一個 function template 時,管用的做法是簡單地為它添加一個重載版本。 

一般而言,重載 function template 沒有問題。但 std 是個特殊的命名空間 。客戶可以全特化std内的template , 但不能添加 新的template(其實是任何東西)到std裡 。

那麼,如何是好?    我們要讓其他人調用swap時能取得我們提供的高效的template特定版本。  還是聲明一個 non-member swap 讓他調用 member swap, 但不再将那個non-member swap 聲明為 std::swap 的特化版或重載版。

假設 Widget 所有相關機能都在 命名空間 WidgetStuff 内, 那整個結果像這樣:

namespace WidgetStuff {

...

template<typename T>

class Widget { ... }; // 同前 ,内含 swap 成員函數

......

template<typename T> //非成員 swap函數

void swap ( Widget& a, Widget& b) // 這裡不屬于std命名空間,是以 根據“實參取決之查找規則” ,Widget位于WidgetStuff命名空間内,就會找到這裡的這個 Widget專用版本swap

{

a.swap(b);

}

}

這個做法對 class 和 class template都行的通。  (如果,沒有額外使用命名空間,上述每件事仍适用。 但,何必在 global 空間内塞滿各式各樣的 class , template,function, enum以及typedef 名稱呢)

但是,你還應該為 class 特化 std::swap。 因為,如果你想讓你的“class專屬版”swap在盡可能多的語境下(就是說,在任何時候都能找到最合适的一個版本)被調用,你需要同時在該 class 所在命名空間内寫一個 non-member版本,及一個 std::swap特化版本。

從客戶角度看,假設你正寫一個 function template, 其内需要置換兩個對象:

template< typename T>

void doSomething ( T& obj1, T& obj2 )

{

...

swap( obj1, obj2 );

...

}

應該調用哪個 swap ?????

我們希望的是:

template<typename T>

void doSomething (T& obj1, T& obj2)

{

using std::swap; //令 std::swap 在此函數内可用

...

swap( obj1 , obj2 ); // 這就可以為 T 類型 調用最佳 swap 版本

...

}

這裡,C++的名稱查找法則 確定将找到 global 作用域 或 T 所在之命名空間内的 任何T專屬swap。

  • 如果 T 是 Widget 類型,并位于 命名空間 WidgetStuff 内, 編譯器使用"實名參數取決之查找法則"( argument-dependent lookup) 找出 WidgetStuff 内的 swap。
  • 如果 T 沒有專屬swap存在,編譯器使用 std内的 swap 。  //  因為使用了using 聲明
  • 如果 T 存在swap專屬 特化版本, 那麼因為你已這對 T 特化了 std::swap, 是以特化版被調用。

【總結】:

  • 如果 swap 預設實作代碼對你的 class 或 class template 提供可接受的效率,你不用做任何事情。
  • 如果 swap 預設版本效率不足 (幾乎總意味着 使用了 指針指向内容 ‘pimpl’ 的手法):
    1. 提供 public swap 成員函數。 這個函數絕不該抛出異常。
    2. 在你的 class 所在命名空間内,提供 non-member swap, 并用它 調用 1 中的 swap 成員函數。
    3. 如果你是class(而不是 class template),  特化 std::swap。并用它 調用 1 中的 swap 成員函數。
    4. 調用swap, 請包涵一個 using 聲明式。 以便 讓std::swap在函數内可見。