effective C++ 條款 29:為“異常安全”而努力是值得的

來源:互聯網
上載者:User

有個class用來表現夾帶背景圖案的GUI菜單單,這個class用於多線程環境:

class PrettyMenu{
public:
    ...
    void changeBackground(std::istream& imgSrc);
    ...
private:
    Mutex mutex;
    Image* bgImage;
    int imageChanges;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    lock(&mutex);
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);
    unlock(&mutex);
}

從異常安全性的角度看,這個函數很糟。“異常安全”有兩個條件:當異常被拋出時,帶有異常安全性的函數會:

不泄露任何資源。上述代碼沒有做到這一點,因為一旦“new Image(imgSrc)”導致異常,對unlock就不會執行,於是互斥器就永遠被把持住了。

不允許資料破壞。如果“new Image(imgSrc)”拋出異常,bgImage就指向一個已被刪除的對象,imageChanges也已被累加,而其實並沒有新的映像被 成功安裝起來。

解決資源泄漏的問題很容易,

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock ml(&mutex);//來自條款14;
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);
}

關於“資源管理類”如Lock,一個最棒的事情是,它們通常使函數更短。較少的代碼就是較好的代碼,因為出錯的機會比較少。

異常安全函數(Exception-safe function)提供以下三個保證之一:

基本承諾:如果異常被拋出,程式內的任何事物仍然保持在有效狀態下。沒有任何對象或資料結構會因此而敗壞,所有對象都處於一種內部前後一致的狀態(例如所有的class約束條件都繼續獲得滿足)。然而程式的現實狀態恐怕不可預料。如上例changeBackground使得一旦有異常被拋出時,PrettyMenu對象可以繼續擁有原背景映像,或是令它擁有某個預設背景映像,但客戶無法預期哪一種情況。如果想知道,它們恐怕必須調用某個成員函數以得知當時的背景映像是什麼。

強烈保證:如果異常被拋出, 程式狀態不改變。如果函數成功,就是完全成功,否則,程式會回複到“調用函數之前”的狀態。

不拋擲(nothrow)保證:承諾絕不拋出異常,因為它們總是能夠完成它們原先承諾的功能。作用於內建類型(如ints,指標等等)上的所有操作都提供nothrow保證。帶著“空白異常明細”的函數必為nothrow函數,其實不盡然

int doSomething() throw(); //”空白異常明細”

這並不是說doSomething絕不會拋出異常,而是說如果拋出異常,將是嚴重錯誤,會有你意想不到的函數被調用。實際上doSomething也許完全沒有提供任何異常保證。函數的聲明式(包括異常明細)並不能告訴你是否它是正確的、可移植的或高效的,也不能告訴你它是否提供任何異常安全性保證。

異常安全碼(Exception-safe code)必須提供上述三種保證之一。否則,它就不具備異常安全性。

一般而言,應該會想提供可實施的最強烈保證。nothrow函數很棒,但我們很難再c part of c++領域中完全沒有調用任何一個可能拋出異常的函數。所以大部分函數而言,抉擇往往落在基本保證和強烈保證之間

對changeBackground而言,首先,從一個類型為Image*的內建指標改為一個“用於資源管理”的智能指標,第二,重新排列changeBackground內的語句次序,使得在更換映像之後再累加imageChanges。

class PrettyMenu{
    ...
    std::tr1::shared_ptr<Image> bgImage;
    ...
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock ml(&mutex);
    bgImage.reset(new Image(imgSrc));
    ++imageChanges;
}

不再需要手動delete舊映像,只有在reset在其參數(也就是“new Image(imgSrc)”的執行結果)被成功產生之後才會被調用。美中不足的是參數imgSrc。如果Image建構函式拋出異常,有可能輸入資料流的讀取記號(read marker)已被移走,而這樣的搬移對程式其餘部分是一種可見的狀態改變。所以在解決這個之前只提供基本點異常安全保證。

有一個一般化的策略很典型會導致強烈保證,被稱為“copy and swap”:為打算修改的對象做一個副本,

在那個副本上做一切必要修改。若有任何修改動作拋出異常,來源物件仍然保持未改變狀態。待所有改變都成功後,再將修改過的副本和原對象在一個不拋出異常的swap中置換

實現上通常是將所有“隸屬對象的資料”從原對象放進另一個對象內,然後賦予來源物件一個指標,指向那個所謂的實現對象(implementation object,即副本)。對PrettyMenu而言,典型的寫法如下:

struct PMImpl{
    std::tr1::shared_ptr<Image> bgImage;
    int imageChanges;
};
class PrettyMenu{
    ...
private:
    Mutex mutex;
    std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    using std::swap;
    Lock ml(&mutex);
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc)); //修改副本
    ++pNew->imageChanges;
    swap(pImpl, pNew);//置換資料
}

copy and swap策略雖然做出“全有或全無”改變的一個好辦法,但一般而言並不保證整個函數有強烈的異常安全性。

如someFunc。使用copy-and-swap策略,但函數還包括對另外連個函數f1和f2的調用:

void somefunc()

{

    …

    f1();

    f2();

    …

}

顯然,如果f1或f2的異常安全性比“強烈保證”低,就很難讓someFunc成為“強烈異常安全”。如果f1和f2都是“強烈異常安全”,情況並不因此好轉。畢竟,如果f1圓滿結束,程式狀態在任何方面都有可能有所改變,因此如果f2隨後拋出異常,程式狀態和someFunc被調用前並不相同,甚至當f2沒有改變任何東西時也是如此。

問題出現在“連帶影響”,如果由函數只操作局部狀態,便相對容易的提供強烈保證,但是函數對“非局部性資料”有連帶影響時,提供強烈保證就困難的多。例如,如果調用f1帶來的影響是某個資料庫被改動了,那就很難讓someFunc具備強烈安全性。另一個主題是效率。copy-and-swap得好用你可能無法(或不願意)供應的時間和空間。所以,“強烈保證”並不是在任何時候都顯得實際。

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

你應該挑選“現實可操作”條件下最強烈等級,只有當你的函數調用了傳統代碼,才別無選擇的將它設為“無任何保證”。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.