您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關怎樣編寫異常安全的C++代碼,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
關于C++中異常的爭論何其多也,但往往是一些不合事實的誤解。異常曾經(jīng)是一個難以用好的語言特性,幸運的是,隨著C++社區(qū)經(jīng)驗的積累,今天我們已經(jīng)有足夠的知識輕松編寫異常安全的代碼了,而且編寫異常安全的代碼一般也不會對性能造成影響。 對象就是屬性聚合加方法,如何判定一個對象的屬性聚合是不是處于邏輯上正確的狀態(tài)呢?這可以通過一系列的斷言,最后下一個結(jié)論說:這個對象的屬性聚合邏輯上是正確的或者是有問題的。這些斷言就是衡量對象屬性聚合對錯的不變式。 我們通常在函數(shù)調(diào)用中,實施不變式的檢查。不變式分為三類:前條件,后條件和不變式。前條件是指在函數(shù)調(diào)用之前,必須滿足的邏輯條件,后條件是函數(shù)調(diào)用后必須滿足的邏輯條件,不變式則是整個函數(shù)執(zhí)行中都必須滿足的條件。在我們的討論中,不變式既是前條件又是后條件。前條件是必須滿足的,如果不滿足,那就是程序邏輯錯誤,后條件則不一定。現(xiàn)在,我們可以用不變式來嚴格定義異常狀況了:滿足前條件,但是無法滿足后條件,即為異常狀況。當且僅當發(fā)生異常狀況時,才拋出異常。 關于何時拋出異常的回答中,并不排斥返回值報告錯誤,而且這兩者是正交的。然而,從我們經(jīng)驗上來說,完全可以在這兩者中加以選擇,這又是為什么呢?事實上,當我們做出這種選擇時,必然意味著接口語意的改變,在不改變接口的情況下,其實是無法選擇的(試試看,用返回值處理構(gòu)造函數(shù)中的錯誤)。通過不變式區(qū)別出正常和異常狀況,還可以更好地提煉接口。 對于異常安全的評定,可分為三個級別:基本保證、強保證和不會失敗。 基本保證:確保出現(xiàn)異常時程序(對象)處于未知但有效的狀態(tài)。所謂有效,即對象的不變式檢查全部通過。 首先從異常情況下資源管理的問題開始.很多人可能都這么干過: Type* obj = new Type;try{ do_something...}catch(...){ delete obj; throw;} 不要這么做!這么做只會使你的代碼看上去混亂,而且會降低效率,這也是一直以來異常名聲不大好的原因之一. 請借助于RAII技術(shù)來完成這樣的工作:
現(xiàn)在來考慮這樣一個構(gòu)造函數(shù): Type() : m_a(new TypeA), m_b(new TypeB){} 假設成員變量m_a和m_b是原始的指針類型,并且和Type內(nèi)的申明順序一致。這樣的代碼是不安全的,它存在資源泄漏問題,構(gòu)造函數(shù)的失敗回滾機制無法應對這樣的問題。如果new TypeB拋出異常,new TypeA返回的資源是得不到釋放機會的.曾經(jīng),很多人用這樣的方法避免異常: 當然,這樣的方法確實是能夠?qū)崿F(xiàn)異常安全的代碼的,而且其中實現(xiàn)思想將是非常重要的,在如何實現(xiàn)強保證的異常安全代碼中會采用這種思想.然而這種做法不夠徹底,至少析構(gòu)函數(shù)還是要手動完成的。我們?nèi)匀豢梢越柚鶵AII技術(shù),把這件事做得更為徹底:shared_ptr<TypeA> m_a; shared_ptr<TypeB> m_b;這樣,我們就可以輕而易舉地寫出異常安全的代碼: 如果你覺得shared_ptr的性能不能滿足要求,可以編寫一個接口類似scoped_ptr的智能指針類,在析構(gòu)函數(shù)中釋放資源即可。如果類設計成不可復制的,也可以直接用scoped_ptr。強烈建議不要把auto_ptr作為數(shù)據(jù)成員使用,scoped_ptr雖然名字不大好,但是至少很安全而且不會導致混亂。 RAII技術(shù)并不僅僅用于上述例子中,所有必須成對出現(xiàn)的操作都可以通過這一技術(shù)完成而不必try...catch.下面的代碼也是常見的: a_lock.lock(); try{ ...} catch(...) {a_lock.unlock();throw;}a_lock.unlock();可以這樣解決,先提供一個成對操作的輔助類: struct scoped_lock{explicit scoped_lock(Lock& lock) : m_l(lock){m_l.lock();}~scoped_lock(){m_l.unlock();}private: Lock& m_l;};然后,代碼只需這樣寫: scoped_lock guard(a_lock);do_something...清晰而優(yōu)雅!繼續(xù)考察這個例子,假設我們并不需要成對操作, 顯然,修改scoped_lock構(gòu)造函數(shù)即可解決問題。然而,往往方法名稱和參數(shù)也不是那么固定的,怎么辦?可以借助這樣一個輔助類: template<typename FEnd, typename FBegin>struct pair_guard{pair_guard(FEnd fe, FBegin fb) : m_fe(fe) {if (fb) fb();}~pair_guard(){m_fe();}private:FEnd m_fe;...//禁止復制};typedef pair_guard<function<void () > , function<void()> > simple_pair_guard;好了,借助boost庫,我們可以這樣來編寫代碼了: simple_pair_guard guard(bind(&Lock::unlock, a_lock), bind(&Lock::lock, a_lock) );do_something...我承認,這樣的代碼不如前面的簡潔和容易理解,但是它更靈活,無論函數(shù)名稱是什么,都可以拿來結(jié)對。我們可以加強對bind的運用,結(jié)合占位符和reference_wrapper,就可以處理函數(shù)參數(shù)、動態(tài)綁定變量。所有我們在catch內(nèi)外的相同工作,交給pair_guard去完成即可。 考察前面的幾個例子,也許你已經(jīng)發(fā)現(xiàn)了,所謂異常安全的代碼,竟然就是如何避免try...catch的代碼,這和直覺似乎是違背的。有些時候,事情就是如此違背直覺。異常是無處不在的,當你不需要關心異?;蛘邿o法處理異常的時候,就應該避免捕獲異常。除非你打算捕獲所有異常,否則,請務必把未處理的異常再次拋出。try...catch的方式固然能夠?qū)懗霎惓0踩拇a,但是那樣的代碼無論是清晰性和效率都是難以忍受的,而這正是很多人抨擊C++異常的理由。在C++的世界,就應該按照C++的法則來行事。 如果按照上述的原則行事,能夠?qū)崿F(xiàn)基本保證了嗎?誠懇地說,基礎設施有了,但技巧上還不夠,讓我們繼續(xù)分析不夠的部分。 對于一個方法常規(guī)的執(zhí)行過程,我們在方法內(nèi)部可能需要多次修改對象狀態(tài),在方法執(zhí)行的中途,對象是可能處于非法狀態(tài)的(非法狀態(tài) != 未知狀態(tài)),如果此時發(fā)生異常,對象將變得無效。利用前述的手段,在pair_guard的析構(gòu)中修復對象是可行的,但缺乏效率,代碼將變得復雜。最好的辦法是......是避免這么作,這么說有點不厚道,但并非毫無道理。當對象處于非法狀態(tài)時,意味著此時此刻對象不能安全重入、不能共享?,F(xiàn)實一點的做法是: a.每一次修改對象,都確保對象處于合法狀態(tài) 在接下來的強保證的討論中細述如何做到這兩點。 強保證是事務性的,這個事務性和數(shù)據(jù)庫的事務性有區(qū)別,也有共通性。實現(xiàn)強保證的原則做法是:在可能失敗的過程中計算出對象的目標狀態(tài),但是不修改對象,在決不失敗的過程中,把對象替換到目標狀態(tài)??疾煲粋€不安全的字符串賦值方法: string& operator=(const string& rsh){if (this != &rsh){myalloc locked_pool(m_data);locked_pool.deallocate(m_data);if (rsh.empty())m_data = NULL;else{m_data = locked_pool.allocate(rsh.size() + 1);never_failed_copy(m_data, rsh.m_data, rsh.size() + 1);}}return *this;}locked_pool是為了鎖定內(nèi)存頁。為了討論的簡單起見,我們假設只有l(wèi)ocked_pool構(gòu)造函數(shù)和allocate是可能拋出異常的,那么這段代碼連基本保證也沒有做到。若allocate失敗,則m_data取值將是非法的。參考上面的b條目,我們可以這樣修改代碼: myalloc locked_pool(m_data);locked_pool.deallocate(m_data); //進入非法狀態(tài)m_data = NULL; //立刻再次回到合法狀態(tài),且不會失敗if(!rsh.empty()){m_data = locked_pool.allocate(rsh.size() + 1);never_failed_memcopy(m_data, rsh.m_data, rsh.size() + 1);}現(xiàn)在,如果locked_pool失敗,對象不發(fā)生改變。如果allocate失敗,對象是一個空字符串,這既不是初始狀態(tài),也不是我們預期的目標狀態(tài),但它是一個合法狀態(tài)。我們闡明了實現(xiàn)基本保證所需要的技巧部分,結(jié)合前述的基礎設施(RAII的運用),完全可以實現(xiàn)基本保證了...哦,其實還是有一點疏漏,不過,那就留到最后吧。 繼續(xù),讓上面的代碼實現(xiàn)強保證: myalloc locked_pool(m_data);char* tmp = NULL;if(!rsh.empty()){tmp = locked_pool.allocate(rsh.size() + 1); never_failed_memcopy(tmp, rsh.m_data, rsh.size() + 1); //先生成目標狀態(tài)}swap(tmp, m_data); //對象安全進入目標狀態(tài)m_alloc.deallocate(tmp); //釋放原有資源強保證的代碼多使用了一個局部變量tmp,先計算出目標狀態(tài)放在tmp中,然后在安全進入目標狀態(tài),這個過程我們并沒有損失什么東西(代碼清晰性,性能等等)??瓷先?,實現(xiàn)強保證并不比基本保證困難多少,一般而言,也確實如此。不過,別太自信,舉一種典型的很難實現(xiàn)強保證的例子,對于區(qū)間操作的強保證: for (itr = range.begin(); itr != range.end(); ++itr){itr->do_something();}如果某個do_something失敗了,range將處于什么狀態(tài)?這段代碼仍然做到了基本保證,但不是強保證的,根據(jù)實現(xiàn)強保證的基本原則,我們可以這么做: tmp = range;for (itr = tmp.begin(); itr != tmp.end(); ++itr){itr->do_something();}swap(tmp, range);似乎很簡單??!呵呵,這樣的做法并非不可取,只是有時候行不通。因為我們額外付出了性能的代價,而且,這個代價可能很大。無論如何,我們闡述了實現(xiàn)強保證的方法,怎么取舍則由您決定了。 接下來討論最后一種異常安全保證:不會失敗。 通常,我們并不需要這么強的安全保證,但是我們至少必須保證三類過程不會失?。何鰳?gòu)函數(shù),釋放類函數(shù),swap。析構(gòu)和釋放函數(shù)不會失敗,這是RAII技術(shù)有效的基石,swap不會失敗,是為了“在決不失敗的過程中,把對象替換到目標狀態(tài)”。我們前面的所有討論都是建立在這三類過程不會失敗的基礎上的,在這里,彌補了上面的那個疏漏。 一般而言,語言內(nèi)部類型的賦值、取地址等運算是不會發(fā)生異常的,上述三類過程邏輯上也是不會發(fā)生異常的。內(nèi)部運算中,除法運算可能拋出異常。但是地址訪問錯通常是一種錯誤,而不是異常,我們本應該在前條件檢查中就發(fā)現(xiàn)的這一點的。所有不會發(fā)生異常操作的簡單累加,仍然不會導致異常。 好了,現(xiàn)在我們可以總結(jié)一下編寫異常安全代碼的幾條準則了: 1.只在應該使用異常的地方拋出異常 另外,還有一些語言細節(jié)問題,因為和這個主題有關也一并列出: 1.不要這樣拋出異常:throw new exception;這將導致內(nèi)存泄漏。 |
關于“怎樣編寫異常安全的C++代碼”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。