1. 程式人生 > >Effective C++讀書筆記(九)實現部分(下)

Effective C++讀書筆記(九)實現部分(下)

Item29 為“異常安全”而努力是值得的

“異常安全”有兩個條件
1)不洩露任何資源。
2)不允許資料敗壞。

例如下列程式碼:

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    lock(&mutex);
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);         //注意,若new丟擲異常,則unlock無法得到執行,資源洩露                                                                    //imangeChanges無法得到恢復,資料敗壞
unlock(&mutex); }

為了避免資源洩露,我們需要適用資源管理類來管理資源。

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock m1(&mutex);
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc); //注意,這時候,不需要主動呼叫unlock了,集中精力對付資料敗壞
}

異常安全函式提供下面三個保證:
1)基本承諾,如果異常被丟擲,程式內的任何事物仍然保持在有效狀態之下。
2)強烈保證,如果異常被丟擲,程式狀態不改變。程式回回復到呼叫函式之前的狀態。
3)不拋擲保證,承諾絕不丟擲異常,因為它們重視能夠完成它們原先承諾的功能。

繼續努力,我們嘗試為changeBackground函式提供強烈保證。

class PrettyMenu
{
    ...
    std::tr1::shared_ptr<Image> bgImage;
    ...
}
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock m1(&mutex);
    //注意,這時候,如果new丟擲異常,則reset函式不會執行,而delete只在reset裡面被呼叫,
    //因此異常不會破壞原資料
    bgImage.reset(new
Image(imgSrc)); ++imageChanges; //如果new丟擲異常,則imageChanges不變 }

知識補充,tr1::shared_ptr::reset方法

// shared_ptr::reset example
#include <iostream>#include <memory>

int main () 
{
    std::shared_ptr<int> sp; // empty

    sp.reset (new int); // takes ownership of pointer
    *sp=10;
    std::cout << *sp << '\n';

    sp.reset (new int); // deletes managed object, acquires new pointer
    *sp=20;
    std::cout << *sp << '\n';

    sp.reset(); // deletes managed object

    return 0;
}

使用copy-and-swap獲取強烈保證

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    using std::swap;
    Lock m1(&mutex);
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));   //這裡製作了一個副本pNew
    pNew->bgImage.reset(new Image(imgSrc));    //改變的均為副本
    ++pNew->imageChanges;                                //改變的均為副本
    swap(pImpl, pNew);                                           //置換資料,changeBackground退出時釋放mutex
}

但看下面的例子,

void someFunc()
{
    ...
    f1();
    f2();
    ...
}

即使someFunc使用的copy and swap技術,但如果f1或f2的執行過程中仍可能導致someFunc沒有達到強烈保證的要求。

另一個主題是這樣實現強烈保證,會損失效率。

當強烈保證不切實際時,你就必須提供“基本保證”。

Item30 透徹瞭解inline的裡裡外外

優點:
呼叫inline函式而又不需要蒙受函式呼叫所導致的額外開銷。
缺點:
會造成程式體積變大,隨之伴隨降低快取記憶體的命中率,從而降低效率。

換個角度說,如果inline函式的本體很小,編譯器針對“函式本體”所產生的碼可能比針對“函式呼叫”所產生的碼更小。inline只是對編譯器的一個申請,不是強制命令。這項申請可以隱喻提出,也可以明確提出。

隱式行內函數,這樣的函式通常是成員函式+友元函式,在函式宣告(標頭檔案)的同時進行定義與實現,顯式行內函數的做法是在函式宣告時加上inline關鍵字。

注意區分行內函數與模板函式,雖然兩者都常常在標頭檔案中實現,但模板函式是為了讓編譯器知道函式呼叫時如何具現話函式而定義在標頭檔案中。

inline函式(即行內函數)對編譯器而言必須是可見的,以便能夠在呼叫點展開該函式,與非inline函式不同的是,inline函式必須在呼叫該函式的每個檔案中定義。當然,對於同一程式的不同檔案,如果inline函數出現的話,其定義必須相同。
正因為如此,建議把inline函式的定義放到標頭檔案中,在每個呼叫該inline函式的檔案中包含該標頭檔案。這種方法保證了每個inline函式只有一個定義,且程式設計師無需複製程式碼,並且不可能在程式的生命週期中引起無意的不匹配的事情。
——摘自《C++ Primer》(第三版)

建構函式和解構函式不要用作inline.

Item31 將檔案的編譯依存關係降至最低

降低檔案的編譯依存關係,需要減少include檔案的數目,考慮前置宣告,但前置宣告存在的問題
1)若標準庫中提供的型別,不能使用前置宣告。
2)編譯器必須在編譯期間知道物件的大小(主要是這一條比較重要)。

Handle class設計
pimpl idion設計,main class只內含一個指標成員,指向其實現類。這種classes內的指標名稱往往就是pImpl. 通過這種設計使得介面與實現分離。

這個分離的關鍵在於以宣告的依存性替換定義的依存性,那正式編譯依存性最小化的本質;現實中讓標頭檔案儘可能自我滿足,萬一做不到,則讓它與其他檔案內的宣告式相依。

1)如果使用object references或object pointers可以完成任務,就不要使用objects.你可以只靠一個型別宣告式就定義出指向該型別的references和pointers;但如果定義某些型別的objects,就需要用到該型別的定義式。
2)如果能夠,儘量以class宣告式替換class定義式。如果能夠將“提供class定義式”(通過#include完成)的義務從“函式宣告所在”只標頭檔案轉移到“內含函式呼叫”之客戶檔案,便可將“並非真正必要之型別定義”與客戶端之間的編譯依存性去掉。
3)為宣告式和定義式提供不同的標頭檔案。一個用於宣告式,僅包括前置宣告。另一個用於定義式,普通標頭檔案。

Interface class設計
另一個方法是使用抽象基類。基類提供工廠方法,用來創建出不同的派生類物件。利用實際的物件來完成具體的工作。

Handle class和Interface class解除了介面和實現的耦合,從而降低檔案之間的編譯依存性。

例子見Item31