標籤:

對使用 C++ 異常處理應具有怎樣的態度?

C++將異常納入標準好多年了,但是直到現在都能看到很多堅持不顯式使用異常(不考慮STL的各種異常處理,且自己寫的代碼中不包含任何異常處理的部分)的項目。

1) 如何看待這種現象?

2) 如果要使用異常,如何判斷是否應實現代碼的異常中立和異常安全(包括強異常安全和弱異常安全以及明確的無異常)?


異常,是用來報告你能恢復的那一類錯誤的。譬如說socket連server結果讀著讀著斷了,拋一個異常,然後你就去善後。譬如說,用戶輸入了什麼東西,一層一層傳遞下去之後你才能做validate,然後一檢查發現不對,拋一個異常,然後你就去善後。


對於異常捕捉來講,你也只能catch你能處理的那些異常。我覺得STL裡面很多異常就不該catch,譬如說越界啦,這些純屬你代碼寫的有問題。代碼的bug是不能靠catch來解決的,必須讓他暴露出來。


所以,對於異常來說,你能恢復的(socket掉了什麼的),那就catch了恢復。你不能恢復的(譬如說vector越界,或者乾脆不是異常的access violation),那就在main函數裡面catch(...),備份好用戶數據,然後調用API生成dump,發回你公司的伺服器,然後讓程序結束。


很多C++程序員的問題,是在於catch了異常之後試圖恢復他所不知道的那些情況,這個時候數據結構都已經亂掉了,你勉強讓程序繼續運行下去,是反社會反人類的。

-------------------------------------------------------------------------

再說了, @陳碩 講的那些什麼構造函數複製構造函數複製函數析構函數什麼的拋了異常,幾乎都是因為代碼寫錯了導致的,根本無需費心思保護,直接dump完掛掉就是。何必自尋煩惱。


整個 C++ exception 的行為在常見語言中是最奇葩的, 因為這個語言特性與 C++ 其他 feature(特別是確定性析構) 格格不入。在 C++ 中全面鋪開使用異常會遇到其他語言中不存在的問題。

從網上容易找到一些公司/組織的C++編碼規範,其中至少 Google、Mozilla、Qt、LLVM 這幾家的規範是明確禁用異常的。前面三家或許可以用代碼歷史包袱、程序員C++水平參差不齊、保證可移植性等理由來解釋,但是 LLVM 卻不同。首先,LLVM 在 2003 年才發布第一版,是個21世紀的新項目,沒什麼歷史包袱;更重要的是,LLVM 的作者同時也開發了 clang 這個 C++ 編譯器,用 C++ 寫 C++ 編譯器的程序員恐怕是 C++ 程序員里對語言掌握得最好的那一批,如果他們都在項目中明確地禁用異常,這意味著什麼呢?注意到 clang 源碼已經用上了 C++11,那麼「考慮移植性照顧老host編譯器」這條理由似乎也不成立了。

C++ 引入異常的原因之一是為了能讓構造函數報錯(析構函數不能拋異常這是大家都知道的常識),畢竟構造函數沒有返回值,沒有異常的話調用方如何得知對象構造是否成功呢?但是編譯器/標準庫為了讓構造函數能拋異常卻是麻煩重重:

  1. 數組元素構造時拋異常,前面已經構造好的元素要析構,還沒有構造的元素不能析構。
  2. 構造函數的初始化列表裡拋異常,前面已經構造好的成員和基類子對象要析構,還沒有構造的成員則不能析構。而且這個異常捕獲之後必須重新拋出(編譯器強制),因為C++不允許「半吊子」構造的對象存在。
  3. 多繼承中某個基類的構造函數拋異常,那麼已經構造好的基類子對象要析構,還沒有構造的基類子對象則不能析構。虛擬繼承,虛基類只能析構一次,你慢慢想吧。
  4. 函數實參對象構造時拋異常,那麼多個實參中已經構造好的實參對象要析構,尚未構造的實參對象不能析構。
  5. std::vector 在 resizing 的時候某個元素的拷貝發生異常,那麼前面已經拷貝的元素要析構,尚未拷貝的元素則不必也不能析構,去看 gcc vector::_M_insert_aux 的代碼有多麻煩。

(註腳:C++ 引入異常的另一個原因是讓 dynamic_cast&(baseReference) 能報錯,因為沒有 null reference。還有一個原因是讓 overloaded operator 能報錯,畢竟 operator 的返回類型往往無法包含 error code,例如 operator=() 返回的是 Type。C++ 也是唯一一個變數賦值有可能會拋異常的語言,例如 Person s; s = getPersonById(someId);,那麼即便 getPersonById() 不拋異常也不能保證上一句賦值不拋異常。)
(註腳2:C++ 引入異常的政治原因是 Ada 支持異常,而 Ada 是 DoD 的指定官方語言,如果 C++ 不支持異常,那麼 ATT 貝爾實驗室就不能拿 C++ 做 DoD 的項目。)

C++ 編譯器要隨時提防調用某個函數 foo 會拋異常,這會阻止一些優化,也會產生很多累贅的代碼(隨時準備析構那些調用 foo 函數前已經構造好的棧上對象)。因此 C++11 的 noexcept 應該大力推廣。

C++ 的 exception specification 也很雞肋,它不像 Java 那樣在編譯期檢查(Java 似乎也流行使用 unchecked exception 了),而是在運行期檢查,而且違反的後果是直接終止程序,那誰敢用啊?還不如用代碼注釋呢。有的編譯器乾脆就只支持語法而不實現功能(Exception Specifications)。C# 也不支持 exception specification,可見這是一項無用的語言特性,算是編程語言發展歷史上走的彎路吧,可惜 Java/C++ 掉坑裡了。

其他支持異常的語言幾乎都有 GC,拋異常就拋了,不用擔心析構,反正GC管著。只有 C++ 才有 exception safety 需要考慮,其他支持異常的語言都沒有這一概念。

而且 Java 的 try-with-resource,C# 的 using,Python 的 with 在管理 function local scope 對象的生命期(資源、lock 釋放)方面不比 RAII 麻煩。Go defer 要差一些,它是 function 級,不是 block 級,只能對付 return。 不過反正 Go 也沒異常,有點小坑罷了,把函數寫短點就能繞過。

RAII 的優勢在於將對象的生命期管理與其他資源(鎖、文件、網路連接等等)的管理整合,然後通過 smart pointers 一併解決了,這是 C++ 獨一無二的優勢。

如果寫遞歸下降的 parser,那麼內部用異常來報錯似乎是合理的,對外返回一個 error code 即可。


類似看待「新買的手機必須充滿 8 小時以上並重複幾次完全充電放電」

  • 「不使用異常」早期是由於歷史原因,就像早期的 STL 也沒人用一樣;後來就是慣性了。
  • 「項目中堅持不使用異常」很多已有項目是基於「任何地方都不會拋出異常」這一假設開發的,代碼本身沒有做到異常安全,經不起異常的引入。如果堅持引入異常需要對代碼從下到上整個做大修改,得不償失。

陳碩列出的一二三四我覺得沒有意義。因為那實際上是在說「實現起來真困難」而不是「使用起來真困難」,那些東西並不是使用者需要關心的。

「不支持 exception specification」本質上說,是「允許任何函數拋出任何異常」。而 noexcept 則是這種方式的增強版,允許自行指定某個函數在某種情況下不應拋出異常,為編譯器優化提供線索。如果說指定了不應拋出異常,卻在運行時拋出異常,這是代碼的 bug,還是及時崩掉退出找原因吧。

你還在用二段式構造嗎?你還以為 new 會返回 NULL 嗎?你還在為異常導致的泄漏而煩惱嗎?沒有異常的 RAII 是不完整的,沒有 RAII 的異常是不安全的。有人問,你覺得你最喜歡的一段 C++ 代碼是什麼?有人說:

}

(裝完逼就跑,真 TM 刺激……


從實際應用來看,客戶端特別是移動平台(iOS、Android)的C++開發禁止異常的一個重要原因是要避免異常帶來的代碼體積膨脹,因為移動平台對代碼體積非常敏感。

另一個原因是傳統的從C繼承而來的返回錯誤碼方式處理錯誤遠比拋異常流行,大家都習慣了。反正我是幾乎沒有用過C++的異常處理,沒有必要。


我個人的態度是,如果你從來不catch任何異常,那麼你可以放心大膽的使用異常。畢竟每次異常拋出來你的程序都退出了。


C++ 引入 exception 的同時,也引入了一個十分模糊的概念 —— exception-safe 。很多人認為有 RAII 就足夠 exception-safe,其實不然。比如 std::vector 對 move 的處理就不止 RAII。Exception-safe 是一個沒有明確定義的概念。

換句話說,exception-safe 是對數據 consistency 的一個拙略的描述(Wiki 上所謂的 weak-exception-safe 其實沒什麼用。你連 consistency 都不保證了,你的程序都處於 unpredictable behavior 了,還 leak 不 leak 的?)。GC 在純內存資源的限制下可以做到 eventual-consistent。RAII 在單個資源的情況下可以模擬 transaction。但是沒有一個簡單的方法能真正做到在使用 exception 的同時保持 consistency。關係資料庫的真正的 two-phase commit transaction 可以保持 exception 下資料庫的 consistency。所以 exception 只在三中情況下好用:

  • 有 GC 並且只處理內存數據。
  • 一次只處理一個數據。
  • 只處理有嚴格 two-phase commit transaction 的數據,例如 RDBMS 管理所有數據。

鑒於 C++ 稱自己為「通用語言」,而且沒有 GC,並不是像當年 Sun 說的 Java 適合 DB-driven 的 server-side 開發。我覺得引入 exception 是很差的決定。

Error code 是用 return-safe 來代替 exception-safe。雖然保證 return-safe 也不容易,但比 exception-safe 容易太多(如果不是「feasible vs. infeasible」的區別)。因為 return-safe 的邏輯和整個代碼的 flow 是統一的。而 exception 要求程序員在無法清晰表述 error 發生的位置的時候用統一的 handling 從錯誤中恢復(否則你就每一行代碼都要 encompass 一個 try-catch block)。在沒有理論支持的基礎上,ad-hoc 比 superficially elegant 更實用更有效。


拋異常的代碼一定要本著:「我被騙了(該assert的事情沒有保證到,例如enable_share_from_this卻沒有用shared_ptr去管理),我對整塊代碼上下文流程都不信任了,我要殺你(這個進程)!「的態度去拋異常。


catch 異常的代碼一定要本著:」我完全理解你的憤怒,我信任你應該沒有什麼事情干到一半卻沒回滾,並且我能夠處理或恢復你嘗試殺人帶來的一系列析構所造成的副作用,你該乾的活沒幹我也可以不care,進程也完全能夠接受因為處理catch而帶來的時間開銷。所以我不讓你殺進程。「這樣的態度去接異常。


可以看出來,catch異常的(對代碼流程和拋異常的原因的了解)要求比拋異常要高很多,所以不推薦隨便的去catch異常,除非是你覺得有問題的第三方庫卻不得不用的。

----------------------------------------------------------------------------------------------------------------------

個人覺得,如果c++只有throw 而沒有try catch 也是挺好的。(如果在析構函數里產生異常,編譯器不允許他離開析構函數體,在析構函數體里終止進程而不是回到main函數在終止。)


不太同意陳碩的結論,雖然他的論據都對。
陳碩指出的1,2,3,4,5確實很微妙,但他們是異常處理的本質困難,GC 語言只在資源泄露上有所幫助,而1,2,3,4,5都涉及一致性問題,這裡 C++ 的考慮是有意義的,其他語言不考慮很可能只是因為他們處理的問題不夠 critical。
另外,前面也有答案說了,1,2,3,4,5主要是實現問題,對於使用者來說,其實並沒有那麼麻煩,至少保證無資源泄露這個層次的異常安全不難做到,一般保證了這個,同時還保證了單一資源(一個對象持有的一組資源)的一致性,這個其實是挺酷的事情。
關於錯誤碼和異常的比較,我覺得都和 C++ 沒多少關係,是異常機制的一般問題,工程上至少有一點異常比錯誤碼要好,那就是異常不會被無意地忽略,而錯誤碼通常會。


看到 很多同學都對C++ 異常處理持否定的態度,但在我看來未必如此! 討論此問題,得分解成兩個問題來討論!

  • 對於編程語言來說,異常處理是否是好的設計?
  • 對於C++來於,異常處理是否是好的設計 ?

討論 exception, 還得從 錯誤處理來討論。

對於錯誤,有三種處理策略。

  • 修正重試
  • 忽略
  • 終止

舉個例子:
1 uber 中心服務,需要通知某個司機,沒有響應超時。 那麼增加超時重試。這是 修正重試;
2 如果還是失敗,那麼就忽略此司機,換下一個。這是 忽略
3 如果一個司機都沒有通知到,那麼服務就應該停止通知並報警。 這是 終止。

怎麼處理,是由調用者的信息、職責來決定的。 不同層次調用者信息和職責都不同。

來舉個例子, IP檢查函數 :

ip_check( const char * ip ) ; //是否合法 ;

怎麼來設計它的錯誤處理?通過返回bool 值,說明是否合法?

  • A 如果,在IP地址的統計程序,這是合理的。 通過bool 值,來忽略錯誤IP.
  • B 如果,在服務程序用於檢查 MYSQL 配置呢? 如果不合法,那麼程序應該終止!
  • C 如果,在一個程序,同時有上面兩種情況呢?….

我會這樣設計:
C++


#include &
#include &
#include &

template & bool ip_check( const char * ip )
{
if ( true ) //示例
{
throw T(ip) ;
}
return false;
}
bool ip_check(const char* ip)
{
return true ;
}

int main (void )
{

try {

ip_check("192.168.0") ;
ip_check&("192.168.0") ;
}
catch(std::exception e )
{
std::cout&<&< e.what() &<&< " "; } return 0 ; }

PHP


//PHP
fucntion ip_check($ip , $exception_cls = null ) ;

//check mysql address
ip_check(mysql_addr, LogicException) ;

//check ip item ;
if (! ip_check(item)) contiune ;

因此如果調用者,需要終止邏輯,就應該使用 exception ; 因為調用者不會每次檢查返回值,並且終止返回( 看看C的代碼,有多少返回值被忽略)。

總結來說:
程序需要一種可以終止的錯誤處理機制,並且更上層的調用者還有機會來處理。

為什麼還有給上層的機會來處理?

  • 還是上面的例子,雖然 Mysql A 的IP 錯誤,但我還是有其它Mysql 備份,程序還是有機會嘗試 B,C 伺服器 ;
  • 在商城上買東西,出現賬戶異常(餘額不足、凍結),交易只能終止,但商品還要保留,其他用戶交易也不能受影響 ;

這就 exception 設計出來解決的問題。exception 只能用於終止策略。 但廣大的程序員都沒有正確的使用異常!

exception safety 是申請資源要在exception 下正確釋放。 這不僅僅是內存,還包括文件、網路 等資源。 有了GC 就並不是就沒有 exception safe 的問題。 C ++ 使用 smarty_ptr ,內存池 可以很好解決 內存的釋放。

C# , Java , PHP, Python ,C++ 都有 exception 機制 ;新設計的 Go, RUST 也有 異常的機制,只是叫panic


對於C++來於,異常處理是否是好的設計? 以後再答! T T


要弄清楚什麼是異常和錯誤檢測,很多人把代碼執行過程中條件檢測錯誤也當異常拋出而不是返回一個錯誤碼,這做法本身就是不妥當。個人認為,所謂導常就是程序執行到這行代碼有問題,但又不知道接下來幹什麼,也不知道返回什麼錯誤或根本就無法返回錯誤,這個時候就只能拋異常給上層調用者了。如果程序檢測到出錯了能夠返回一個錯誤碼,為什麼要拋異常?不要用拋異常來替代返回錯誤碼!


很雞肋的東西,是唯一編譯器前端完全掌控,編譯器後端看不到的東西。

我個人不推崇C++ Exception,我覺得這算是一個語言發展走的彎路。


C++中的異常是必不可少的!如果說因為異常導致結構複雜,那沒有異常就簡直是災難。假設沒有異常的情況

template&
void foo(const T t)
{
an_internal_cont.push_back(t); //copy-constructor錯誤.
//現在怎麼辦?我怎麼知道an_internal_cont.back()是不是正確的呢?
}
難道要C++教科書里寫一個教條,所有類型必須定義一個名叫check()判斷這個對象是否正確的方法?(那麼問題來了,copy-construction都錯了,怎麼能保證check()不錯?)其實一些人提到在構造函數里加一個參數判斷是否構造成功,如果這作為一種手法,就跟這「教條」一樣愚蠢,因為這招式太愚蠢了

另外異常確實會降低設計的難度。例如

void foo() //異常中立
{
a_lib.foo();
b_lib.foo();
}

如果沒有異常

error_code foo()
{
int a_err = a_lib.foo();
if(a_err)
return a_err_to_errcode(a_err);

int b_err = b_lib.foo();
if(b_err)
return b_err_to_errcode(b_err);

return success;
}

然後你在定義error_code的時候
enum error_code
{
錯誤碼要涵蓋a_lib的一部分,還要涵蓋b_lib一部分。還不要搞混淆了
}

看到這兩個例子,你們是不是覺得應該全面鋪開使用異常呢?


我是來反對馮東這個答主的,但首先回答題主的問題

1) 如何看待這種現象?

我對exception的看法就是,你覺得想用就用,不想用就不用。有需求就用,沒需求你的程序構架設計隨意。

2) 如果要使用異常,如何判斷是否應實現代碼的異常中立和異常安全(包括強異常安全和弱異常安全以及明確的無異常)?

一般代碼是不需要強安全之類的異常安全的。如何判斷,只和業務流程有關。業務流程懂了,你自然就知道哪裡需要哪裡不需要。

----------------

我就是來反對 馮東 這個答主的。我在評論裡面說了,答主的回答實在不地道。然後他就把我拉黑了。這種人也有這麼多人關注,實在是無語了。

C++ 引入 exception 的同時,也引入了一個十分模糊的概念 —— exception-safe 。很多人認為有 RAII 就足夠
exception-safe,其實不然。比如 std::vector 對 move 的處理就不止 RAII。Exception-safe
是一個沒有明確定義的概念。

名詞形容詞不分。 應該如下。

C++ 引入 exception 的同時,也引入了一個十分模糊的概念 —— exception-safety 。很多人認為有 RAII 就足夠
exception-safe,其實不然。比如 std::vector 對 move 的處理就不止 RAII。Exception-safety
是一個沒有明確定義的概念。

有人說你太吹毛求疵了,拜託,這位答主滿篇英文漢語混雜,應該是在國外混的好吧。我也是國外混出來的。滿篇混雜沒關係,你連英文都不利索,還混雜啥。(抱歉我滿滿的惡意,我在評論里沒有這麼mean,他還是直接拉黑我,我就是看不慣他)

換句話說,exception-safe 是對數據 consistency 的一個拙略的描述。

Exception-safety從來就和consistency是2個概念。題主愣是把這2個概念說成一個,說exception-safety是consistency的拙劣描述,我讀了半天真的都糊塗了。我還以為我知識不夠,最起碼我上過distributed system和distributed database這幾門課好吧。搞得我又去搜了半天,發現只有題主一個人這樣混淆。

Exception-safety的概念原paper和wiki都有解釋,我就不多說了,自己搜。再多說兩句,exception-safety並不是官方概念,是C++的一些人單獨提出來的(David Abrahams提出,Herb Sutter等推廣),概念上也可以應用於其他語言的異常處理。

你可以說部分strong exception-safety的要求和data consistency的要求重合。什麼拙劣描述,說的太主觀了。並且很多人說exception-safety都是指weak-safety好吧。

GC
在純內存資源的限制下可以做到 eventual-consistent。

恕我知識不足(我這裡確實看不懂)。Eventual consistent,我搜了半天,嗯,

"Eventual consistency is a consistency model used in distributed computing
to achieve high availability that informally guarantees that, if no new
updates are made to a given data item, eventually all accesses to that
item will return the last updated value."

恕我才疏學淺,我實在找不到GC和eventual consistency的關係。google搜索GC+eventual consistency也搜不到什麼共同點。。請大神指教(當然馮東你屏蔽我了,你就別來了)。

RAII 在單個資源的情況下可以模擬
transaction。但是沒有一個簡單的方法能真正做到在使用 exception 的同時保持 consistency。關係資料庫的真正的
two-phase commit transaction 可以保持 exception 下資料庫的 consistency。所以
exception 只在三中情況下好用:

  • 有 GC 並且只處理內存數據。
  • 一次只處理一個數據。
  • 只處理有嚴格 two-phase commit transaction 的數據,例如 RDBMS 管理所有數據。

我不是專家,我想答主想表達的是,上述三種情況,可以保證consistency?答主貌似把transaction consistency,data consistency搞混了(其實我不知道他想表達什麼)?並且答主直接把consistency和exception safety概念混用了?反正我看著蠻凌亂的。不知道答主在哪裡學到這些,給我個資料讓我讀讀吧。哦,對,答主把我屏蔽了。

鑒於 C++ 稱自己為「通用語言」,而且沒有 GC,並不是像當年 Sun 說的 Java 適合 DB-driven 的 server-side 開發。我覺得引入 exception 是很差的決定。

看到這裡,難道答主是專門搞server-side db 開發的?所以對trasaction consistency有要求? 我感覺我要猜測答主的背景,我才能知道答主想表達什麼。

Error
code 是用 return-safe 來代替 exception-safe。雖然保證 return-safe 也不容易,但比
exception-safe 容易太多

return-safety,我能理解答主想說什麼,但是google return-safety貌似真的沒人提這個詞。答主自創?

(如果不是「feasible vs. infeasible」的區別)。

滿滿的機翻的感覺,好吧,我吹毛求疵了。

因為 return-safe
的邏輯和整個代碼的 flow 是統一的。而 exception 要求程序員在無法清晰表述 error 發生的位置的時候用統一的 handling
從錯誤中恢復(否則你就每一行代碼都要 encompass 一個 try-catch block)。

我不知道題主想說什麼,但是在複雜系統中,exception一旦沒有當場catch,絕大多數情況下是無法恢復的。你用error code,如果經過好幾層的return之後,也不好恢復了吧!!

在沒有理論支持的基礎上,ad-hoc 比
superficially elegant 更實用更有效。

哇,你這是跟我討論哲學么?為什麼ad-hoc就比
superficially elegant更實用更有效? 你這句statement就有理論支持了?quote一下吧,小人不才。哦,答主屏蔽我了,又忘了。


異常的技術問題可以見 @陳碩的答案,我補充一點:

異常不僅導致沒有GC的系統內存泄漏,更嚴重的是很容易導致程序的核心狀態數據不一致/不同步/不完整,從而引發嚴重的bug。這個問題不僅C++存在,任何有異常的系統(Java、C#、Python ...)都存在

解決辦法之一是把所有修改核心狀態數據的代碼設計成事務性的(注意:RIIA/RAII、IDisposable等模式不解決這個問題),不過很少有程序能做到這一點


可適當使用異常,但絕不要作為 error code 來使用,效率太低
適當的在無法完成操作的情況下拋出異常,並在暴露給用戶的介面處對異常進行整體封裝,提供簡單直接的錯誤信息彙報給用戶,即提高開發效率(只在用戶介面處有 try...catch...),又做到用戶友好

異常最大的好處就是避免代碼中充斥著與業務邏輯無關的錯誤處理代碼,使得代碼結構清晰簡潔,提高開發效率。

常見的錯誤使用異常的代碼特徵:

  • try...catch 在代碼中滿天飛,往往都是將異常作為 error code 來用了,每個調用者在調用一個可能拋異常的函數時都 try ... catch ...
  • 逐層向上 rethrow 異常
  • 在循環體內throw、catch異常

很多項目不使用異常我個人覺得還是因為在C++里使用異常有太多的東西要考慮。並且C++的異常,需要花一定的時間和精力去學習和使用。至少比學會使用STL要更有難度。

要使用C++的異常機制,首先你要明白什麼時候該用異常?以及最基本的如何設計自己的異常類。這裡推薦閱讀boost庫的文章:Error and Exception Handling

簡單總結下這篇文章就是:

  • 你要有選擇得使用異常。選與不選的標準看你自己,但是對於時間要求比較高的情況,不應該使用異常(in time critical code, throwing an exception
    should be the exception, not the rule.)。
  • 自己設計的異常類不要包含std::string成員或者是拷貝構造函數會拋異常的其他自定義類型。
  • what()函數的格式化要考慮周全。

假定現在正在用C++開發一個系統,你已經有了幾個依賴庫,他們都是用C語言實現的。而你已經決定使用異常了。在這種情況下,可能的情況應該是:

對於依賴庫C函數返回的錯誤碼(真正的錯誤),要在你的系統邊界直接處理掉。而不是將它轉換為異常。除非你專門給他們做了wrapper,用來將錯誤碼轉換為相應的異常,然後你的系統和這些wrapper打交道。

數據進入到你的系統內部後,用assert來處理函數,模塊等前、後置條件不滿足錯誤。這些錯誤都是bug,是可以通過改程序解決的,不應該用異常。

剛才上面提到了「真正的錯誤」,主要還是考慮到錯誤碼非零,或者著errno非0的時候,並不意味著是錯誤。典型的就是socket。比如非阻塞socket的EAGAIN。這種情況下,你的wrapper不應該將其轉換為異常。這個時候程序的狀態是正確的。

另外,如果你提供了一個類庫給別人用,那麼可能的情況是,對於非法的輸入,你能處理糾正的,就處理糾正了。對於不能自己消化的錯誤,拋出異常。這時候就不是assert了。

關於第二個問題,異常安全。這個就更複雜了。還是看boost庫的文章吧:Exception-Safety in Generic Components

這個我覺得就不是三言兩語能總結得清楚了。異常安全至少包含下面兩層意思:

  1. 發生異常時,沒有資源泄露
  2. 發生異常後,程序狀態正確

異常安全應該也是C++里的難點了。

這裡的難點有:

  1. 三種類型的異常保證本身就比較難理解。除了那個no-throw保證以外。
  2. 所有的資源都應該使用RAII進行管理。而RAII對於很多C++程序員來說,其實好像還是很陌生的(是么?還是不是?這純粹是我對身邊人的感覺)。
  3. 即便是用了RAII,代碼寫的不好也是枉然。例子後面會說。
  4. 析構函數堅決不能拋異常。這就是那個no-throw。很多人知道,但是不知道為什麼。

RAII真的很重要!

最後補充剛才說的那個例子:

void bad()

{
if( shared_ptr&( new int(2)
), g() );

}

我們使用了shared_ptr來管理這個動態分配的內存。這個內存管理使用了RAII慣用法,但是,無法保證異常安全。原因就是參數計算順序在C++里沒有指明。很可能,先動態分配了內存,然後調用g(),然後構造了shared_ptr對象。在這個情況下,如果g()會拋異常,那麼資源就泄露了,也就無法保證異常安全了。

其他的參考資料:

·
shared_ptr - 1.55.0

·
GotW #56: Exception-Safe Function Calls


我對異常的理解是有不在設計邏輯中的錯誤發生,比如連接中斷,可能是網路波動,可能是客戶端意外關閉,甚至可能物理網路故障,比如文件讀寫異常,可能是文件損壞,可能是物理壞道等等,不一而足(對那個因為醫院設備輻射導致計算機數據異常那個故事印象深刻)。對於程序邏輯來說,意外情況數不勝數,不可能給所有狀態都打一個特定的error code返回來,而且有的異常是可以釋放當前資源,然後重試的,有的是無法進行只能結束程序的。

我一般是在可恢復部分用try-catch包裹,發生致命性異常的話就直接扔到最頂層報告錯誤,退出。


***;
if(***)return ***;
***;
if(***)return ***;
***;
if(***)return ***;
***;
if(***)return ***;
………

***;
if(***)goto ***;
***;
if(***)goto ***;
***;
if(***)goto ***;
***;
if(***)goto ***;
return;
***:
………

你們自己選吧


其他語言的異常處理,整個語言是統一的,都採用異常機制。如果接下來整段代碼都可能出錯,那麼整個包裝一個try catch就行。
而c++雖然也支持異常處理機制,但是有大量的歷史代碼採用返回值來判斷操作是否成功,根本沒法用異常機制。那麼實際寫代碼的時候,就不能整段整段的try catch,反而更麻煩。


異常的使用應該關注資源,而不是狀態。
異常與其說是彙報錯誤的發生,不如說是用來回退調用堆棧的,要配合RAII使用。
一般用於資源獲取失敗時,一路向上釋放資源,直至程序資源正常為止。

程序很多異常狀態是邏輯上的問題,異常可以處理,但不是唯一選擇,也不是最好選擇。

比如邏輯上一定成立的,應該用assert檢查,然後用測試例子覆蓋所有輸入,而不是亂拋異常。
邏輯上可能不成立的,應該用返回值處理,而不是拋異常。因為異常能處理的邏輯實際上只有一個,就是出錯了向上回退釋放資源。而依靠返回值能處理的邏輯更廣。可以參考boost asio的設計,一個函數提供兩個介面,返回error_code的和拋異常的。返回error_code的適合即時解決問題,拋異常的適合自己不處理交由別人解決。

邏輯複雜的可以用狀態機管理,比如boost meta state machine。用異常或者error_code管理屬於手上有榔頭看什麼都是釘子。
------------
異常用的比較多的有構造函數,比如創建一個對象,需要依賴文件、網路、資料庫等。如果有一個步驟出問題,那麼對象創建失敗,這時就應該拋異常釋放已有資源。你也可以選擇多步init初始化然後根據返回值判斷,但這很羅嗦,把負擔轉移到使用者身上了。而異常從設計上就是用來簡化這一步驟的,使得構造函數具有一定原子性,簡單、統一。

還有就是向上跳出邏輯、循環、中斷線程等,這種屬於資源不再需要、用拋異常簡化資源釋放的情況。
------------
關於什麼時候應該catch的話,一般是資源的獲取(對象創建,函數調用、各種容器的.at())可能會失敗,且需要處理的時候。這種catch寫哪個層面、粒度粗細、是需要衡量的,沒有定數。

還有就是在析構函數里要catch所有異常,比如在析構函數里打log也可能拋異常,這個異常不應該跑出析構函數的範圍。這是因為c++ RAII的「公理"是資源釋放一定會成功,拋異常就進入不可名狀之狀態了


推薦閱讀:

TAG:編程 | C++ | C / C++ |