知乎上看到一些人評價c++的exception很難用,想問一下大家寫c++時怎麼處理錯誤?
難道像c語言一樣返回-1嗎?
直接return值真的很方便啊,我真的很討厭每寫一句函數就查看一下該值是否等於-1。
從使用上說,如果我面對的是一個事務型的操作,也就是說整個流程可能較為複雜,但是需要保證每一步都成功,最後才成功,只要有一步失敗,則整個流程失敗(一般也需要回退),這種情況下可能更傾向於用異常,代碼寫起來看起來都爽,當然如果需要回退,最好也結合RAII但是如果面對的操作流程本身比較複雜,有大量的選擇性分支,則返回值的辦法更可控些了,另一方面,存在一定比例的操作,我們並不太關注其是否會失敗,比如說,代碼裡面寫日誌的介面,如果在write文件的時候出現IO錯誤(比如硬碟不小心寫滿),則我們可能並不需要去處理,因為這不是一個「常見」且「需要預料」的錯誤,可以通過其他途徑來監控這類問題,如果因為一個debug日誌的異常而中斷整個流程,有時候是不太好的,畢竟伺服器的話,忽略這個錯誤還是可以提供一種相對可靠的服務,當然可以在日誌裡面做個大try,保證永遠不拋異常了簡單說,這兩者的區別就在於,異常機制強制你關注它,不catch就可能掛,而返回值機制則給了一個相對寬鬆的選擇,你可以關注也可以不關注
static_assert()、assert()、return。
1. 別人的錯,用斷言。比如
int randomInt(int lower, int upper)
{
assert(lower &<= upper);
// ...
}
定好文檔,RTFM。函數被調用時 precondition 沒有被滿足,不要為上層的失誤擦屁股。
2. 只在最外層拋棄詳細錯誤信息,無論是錯誤碼還是異常。
檢測到錯誤以後,要麼把錯誤吞掉當作什麼都沒發生,要麼把錯誤報告出去,要麼繼續往上傳。但選擇繼續往上傳時,不要讓錯誤消息縮水,比如把錯誤碼變成 bool。3. 如果代碼用於嵌入式、實時系統,或者是並未使用過異常的已有工程,用異常前請三思。
4. 函數正常執行時有且只有一個需要返回的數據,出錯時建議用異常。int generateId(); // 建議出錯時拋異常
int generateId(); // 真心不建議用特殊值表示出錯,比如 &< 0 為錯誤碼
int generateId(int out); // 如果你堅持這麼做我也沒辦法
bool generateId(int out); // 建議不要這麼做。見第二點
不要為了引入錯誤碼把調用搞複雜。把小於 0 定為錯誤碼相當於用蹩腳的方式實現了 optional,並且沒法從簽名中看出來,必須藉助文檔。
5. 與抽象資源相關的錯誤,用異常。用 RAII 封裝這些「資源」,但不要用二段式構造。
理解 RAII。要做到異常安全,RAII 是必不可少的。要實現真正的 RAII,異常是不可或缺的。6. 只在必要時 catch 異常,比如:
- 析構函數不能拋出異常:不解釋
- API 邊界:比如動態鏈接庫需要 C 風格介面(此時還需要做異常到錯誤碼的轉換)
- 文檔中明確定義不能拋出異常的函數:比如某些操作系統的回調函數
- 決定報告錯誤:其實廣義而言,也是 API 邊界的一種
- 決定吞掉錯誤:出這個錯無所謂
- 有從錯誤中恢復的方法:比如用 UTF-8 解碼文本失敗,轉而嘗試用 GBK 解碼
return -1是C時代的古董,到C++里如果再大量使用這種錯誤處理技巧就是在寫C-Style C++代碼。至於C++的throw-catch性能問題,我只想說沒人閑的蛋疼產生錯誤不修復而靠錯誤系統來修補,我在寫代碼時一律throw __func__然後在main裡面catch(const char*)再用cerr輸出,這樣錯誤就很清楚了,直到沒有throw發生才代表寫出來的代碼是沒有錯誤的。
我覺得還是要搞清楚哪些是錯誤哪些是異常吧。
引用並稍微修改了一下我之前的一篇文章:C++異常處理的是與非
關於C++的異常處理,眾說紛紜,而且經常出現有部分人強烈支持的、有部分人強烈反對。
總結一下,異常的優點與缺點如下:
相較於錯誤碼的處理方式,使用異常的好處是除非顯式指明忽略,它會中止程序的進一步運行。
比如,C++的new在內存不足時默認的處理方式就是拋出異常,如果用戶沒顯式的處理該異常,程序就會直接core掉,讓用戶很清楚的能從core文件中分析出程序掛在哪兒了。
而malloc則不然,在用戶沒有顯示處理時,默認是情況下會忽略掉該錯誤,這樣,就很有可能在程序錯誤運行了半個小時之後,當真正用到這個malloc得到的指針時才core掉。
而這個情況其實還算好的,至少程序還會在你用這個指針時core掉,而有的時候,沒準就因為一個返回值沒檢查,你已經錯誤的運行了很久以後才知道之前早就出錯了,這時候程序的棧早就亂得不知道是啥樣兒了,而且錯誤運行那麼久,沒準已經錯誤的把許多不該修改的外部存儲狀態都修改得亂七八糟了。
異常處理能使正常代碼與錯誤處理的代碼分離,使代碼清晰這句話成立的前提是:不要使用異常來進行正常的跳轉。
如果你正確的使用異常,就可以只在catch中進行錯誤的處理、恢復,使代碼的邏輯更清晰。
但如果你偏偏要用異常來做正常的跳轉,那就會起來完全相反的效果,那錯誤處理、恢複流程與正確流程將比錯誤碼的方式更難區分。
如果禁用異常,當構造失敗時,常見的辦法是通過一個init方法來初始化對象,然後通過判斷init的返回值判斷構造是否成功。
然後,我們會發現,當這個對象析構的時候,我們又需要通過一些方法確定哪部分析構操作該執行,哪部分操作不該執行。此處就很容易出現問題,經常會過多的執行一些不該執行的析構操作。
而在構造函數中拋出異常則不會調用該對象的析構函數,用戶只需要在構造函數中恢復自己對外部造成的惡劣影響即可。
許多操作符重載只能使用異常來報告錯誤操作符重載在C++中也是處於一個比較尷尬的地位,但相對於異常處理的眾說紛紜,操作符重載還是相對容易達成共識的:
只有當重載運算符對該類很自然時才重載,絕不貪圖使用方便而盲目重載運算符。
而許多時候,為了使操作符重載更為自然,就需要將操作符的使用模擬得與原生的操作符基本一致。
這時,就導致一個問題,如果操作符重載時,出現異常該如何返回?總不能為了一個加法操作符而在返回值里加一個標誌位吧?
基本上只有異常能拯救你。
關於這點,在百度的編碼規範中提到測試表明,一個單線程程序每秒能拋出、接住25萬條異常,比打一條日誌還快。(這裡指的異常還是指的經過公司封裝之後的一個異常類,其中添加了一些自定義的用於分析錯誤的信息)
只有在你的程序拋出異常太多的情況下才會真正影響到性能,這個擔心可能是不必要的,只需要你記住前面已經提過的一句話:「不要使用異常來進行正常的跳轉」。你的程序中就不可能出現那麼多異常。如果即使這樣,你的程序每秒還是拋出幾萬條異常,那你還是查查你的程序是不是有啥嚴重的問題吧。
在一些情況下導致代碼可讀性變差,或者導致代碼重複參見zeromq作者的一篇文章:
Why should I have written ZeroMQ in C, not C++ (part I)
實際上我認為martin sustrik舉的例子恰好是用異常來解決正常的邏輯,他提到的許多時候,異常拋出到上層之後,我們需要在不同的調用處用同一段邏輯來解決這個異常,導致代碼的重複。其實這種用法的根本原因在於,在不恰當的場景下使用了異常。一般只有出現錯誤,而本函數內不能確定該如何處理該異常,才會將其拋出;當異常拋出時可以清楚的確定這個錯誤該如何恢復時,這個錯誤恢復的過程應該在函數內部完成,而非拋出異常。
許多時候寫出異常安全的代碼是相當困難的比如以下代碼:
如果不使用異常,它就是很安全的,可foo里拋出了異常就很有可能導致file沒有關閉。
如果想避免這種問題,就需要使用RAII機制,關於RAII請參考我的另一篇文章:http://blog.acmol.com/_sources/pages/cpp-raii.txt
Google在編碼規範中提到:
異常安全要求同時採用 RAII 和不同編程實踐. 要想輕鬆編寫正確的異常安全代碼, 需要大量的支撐機制配合. 另外, 要避免代碼讀者去理解整個調用結構圖, 異常安全代碼必須把寫持久化狀態的邏輯部分隔離到 「提交」 階段。它在帶來好處的同時, 還有成本 (也許你不得不為了隔離 「提交」 而整出令人費解的代碼). 允許使用異常會驅使我們不斷為此付出代價, 即使我們覺得這很不划算。
不過Google到最後還是說,表面看來使用異常帶來的好處是比壞處要大的多的。但是,由於公司里一堆堆舊的,異常不安全的項目,為了兼容老的異常不安全的項目,新的項目也盡量別使用異常了。
(參見http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions)
也就是說最終google是因為有一大堆歷史包袱以及項目管理的原因而禁止使用異常的,對於沒歷史包袱的小公司,如果你的成員水平都還可以,能夠學會RAII機制,適當時候使用異常是一個很好的選擇,如果成員水平參差不齊,連RAII都掌握不了,那還是別用了。
我throw出來的大部分異常其實都是為了區分各種assert,所以是不catch的。一旦拋了這種異常就代表我程序寫的不對,有bug要修,這個時候就要改代碼,不要去想怎麼做錯誤恢復,掛了就掛了。有catch的那一小部分異常都是我的腳本引擎和GUI庫throw出來的。
對此我給他分別寫了兩個基類,一個是Error,一個是Exception。
如果你在構造函數裡面拋了異常,如果你的類成員都是通過初始化列表來初始化的,那其實不用理。如果你的類成員是在構造函數裡面賦值的(多半是指針),你得catch,刪掉他們,然後rethrow。這幾個標準動作你只要多做幾次很容易就熟練了。然後你就會真正掌握一種簡單的、流暢的C++編碼方法,每一行都是異常安全的。
1、異常有什麼用:
首先請明確,異常的根本作用是跳轉,不要看到異常就認為是出錯,程序不出錯你一樣可以throw2、異常有什麼其他方法替代不了的地方:
跨層傳遞信息並且保證信息不丟失。讓信息在它應該被捕捉的那一層捕捉。
int fun3()
{
return -1;
}
void fun2()
{
fun3()
}
void fun1()
{
fun2()
}
假設以上三段代碼分別是三個不同的人寫的。
fun3返回-1表示出錯了。
但是如果fun1想知道出錯了,需要fun2裡面return fun3(),而不能直接調用fun3()就沒有了。
直接調用fun3()就導致了錯誤碼的丟失,上層邏輯由於寫fun2的人的疏忽,不知道到底是什麼錯。
但是如果是異常機制,fun3拋出異常,fun2疏忽了,沒有try,catch,這個異常不會丟,會繼續拋到fun1。
也就是fun2不想關心出了什麼錯,就可以不關心。
fun1想關心,也不會因為fun2的不關心而導致關心不到,鞭長莫及。
3、異常怎麼用:
盡量不用,就像C裡面的goto一樣,本質都是跳轉。
參考《代碼大全》中的一段話:僅在其他編碼實踐方法無法解決的情況下,才使用異常。一些個人建議,僅供參考
1、在你自己的函數中,檢查別人的傳入參數是你的責任。
2、調用別人的函數,檢查你自己傳入的參數的合法性是你的責任,檢查它的返回值也是你的責任。
3、別人的代碼會拋出異常,不代表你就真的讓他拋一個異常出來。我相信它肯定可以不拋出異常的。
4、不要用異常去代替合法性檢查,那是推卸責任。
5、程序有bug是正常的,程序出現問題是正常的,依賴於異常來處理問題是不正常的。
6、一個try的代價,不一定比你寫一個if的代價小,請參考《More Effective C++》第15條。
7、構造函數出錯咋辦,如果你處理不好構造,用Init代替構造,用Release代替析構,並確保這兩個函數被調用。
8、注意變數初始化了再使用,給定一個安全的默認值,保證即使用戶給了一個錯誤的值,你也可以按默認值跑通你的流程。
難道像c語言一樣返回-1嗎?
直接return值真的很方便啊,我真的很討厭每寫一句函數就查看一下該值是否等於-1。
你的這個想法就是將異常加入C++的人的心聲。不要相信那些「盡量不要用異常」的**言論。現在是2016年,不是1999年。這種言論只在極其有限的條件下有用。
不用異常,你就得這樣:int foo()
{
// Other code
if (foo1(var1, var2) == -1)
return -1;
if (foo2(var3, var4) == -1)
return -2;
if (foo3(var5, var6, sometype* output) == -1)
return -3;
// Other code
}
用了異常,你可以這樣:
void foo()
{
// Other code
try
{
foo1(var1, var2);
foo2(var3, var4);
result = foo3(var5, var6);
}
catch(SomeError err1)
{
// Do sth or rethrow
}
// Other code
}
- 不用異常,只要有可能報錯的函數,你就要在外面圍一個if。代碼噪音震耳欲聾。
- 不用異常,foo3的函數原型都要改,返回值位置要留給錯誤碼,真正的結果只能靠指針參數傳遞,醜陋很多。
而這僅僅是最基礎的好處。
如果一個函數自己不能處理底層錯誤,只需要把錯誤轉發給上層,差別更大。不用異常,代碼是這樣的。void foo();
int foo2();
int foo3();
void foo()
{
if (foo2() != 0)
{
// Recover
}
}
int foo2()
{
// Other code
if (foo3() != 0)
{
return -1;
}
// Other code
}
int foo3()
{
// Other code
if (someStatus != 0)
return -1;
// Other code
}
用異常時,代碼是這樣的。
void foo();
void foo2();
void foo3();
void foo()
{
try
{
// Other code
foo2();
// Other code
}
catch (SomeError err)
{
// Recover
}
}
void foo2()
{
// Other code
foo3();
// Other code
}
void foo3()
{
// Other code
if (someStatus != 0)
throw SomeError("Some status is not in correct value.", someStatus);
// Other code
}
- 不用異常,不僅foo2,foo3的原型要改,而且foo2還為轉發foo3出產生的錯誤增加了額外的代碼。
- 不用異常,foo3中不正常的status怎麼傳遞給foo也成了大問題。你不能依賴於foo3和foo2的返回值層層返回,因為foo3也許可能會產生多種錯誤,而foo2也有可能調用多個可能返回錯誤碼的函數。可行的方案只有依賴全局變數。而全局變數隨時可能會被其他函數甚至其他線程修改。而用異常時status的值可以輕鬆塞進異常對象。
不用異常,只有以下條件下是合理的:
你在維護古董代碼,重寫已經不可能了。異常會造成資源泄露是一種很傻逼的言論。管理資源是基本功,基本功不過關,用不用異常都會泄露,異常最多會讓你到時候死更慘一點。如果工作單位在維護古董代碼以外的場合仍然禁用異常,你需要考慮這家公司的水平是不是值得你一直呆下去了。如果你覺得異常拖慢了你的程序,那你應該反省一下,為什麼你的程序會錯誤滿天飛。有很多方法:
1:不處理,直接死。2:返回一個bool,適應於簡單的情況。3:返回一個異常對象。4:在參數里放一個二級指針,用於攜帶異常對象。另外,printf大法好!!!(語言中的)異常是一個不完整的東西,它其實就是遠程跳轉(即c的lonjmp),當然實現中只能在一個線程中往回跳。這雖然滿足了異常提出的初衷,即在高的調用層次中處理問題,避免層層檢查和轉發的繁瑣和額外開銷,但同時存在很多的限制。
本質上異常就是事件的一種,採用一個良好的事件處理機制來包容它們就可以了。看到很多人建議能不用異常就不用異常,其實,這是一種對異常的誤解。
避免使用異常的其中一個依據是異常有性能的代價。但是基本上大家都不清楚異常帶來的性能代價有多少。異常的確會帶來一些開銷,但是,和大家想像中的不一樣,並不是在任何時候使用try/catch都會有性能上的開銷。據我所知,如果大家用的是gcc編譯器的話,那麼,如果你的代碼沒有濫用異常的話,基本上不會有異常導致的性能問題,gcc採用了一種zero cost的機制去生成C++異常處理相關的代碼,所謂zero cost並不是指異常處理沒有代價,而是指,在程序執行的過程中,在沒有異常發生的情況下,你的程序不需要為此付出額外的代價, 簡單的理解,就是:對於非異常的執行路徑,無論你的代碼中有多少try/catch,和沒有try/catch的結果是一樣的,多一個try/catch不會導致程序會多執行一條指令。
不過,如果有異常產生的話,那麼,這個代價就相當可觀了,可以說,遠遠超過了C語言中通過返回值的方式,其中最大的開銷應該是退棧的過程,在這個過程中異常處理代碼需要去遍歷進程中所有的模塊並根據PC去匹配相關的異常處理器,而在遍歷模塊的時候又不可避免的需要獲取全局鎖。需要注意:異常處理器不僅僅存在於有catch的代碼中,而是存在於所有在退棧過程中需要析構局部對象的函數中。我相信gcc對C++異常的處理方式是基於這樣的假定,在程序的運行過程中,出現異常的概率是很低的。所以,如果你的程序符合這樣的假設,那麼用異常處理是基本上不會有問題的。當然了,如果你把異常當goto來用,把它作為普通的邏輯轉移的一種,那麼,只能說那是你自己的問題。我在某個C++的項目中就碰到過這種情況,有人在一個頻繁被多個線程調用的函數中把throw當return來用,結果程序跑起來,用gstack一看,發現很多線程都在等待一個全局鎖,而那個鎖就是異常處理中用來遍歷模塊的。不過微軟的編譯器好像並沒有採用同樣的方式去處理異常。微軟的編譯器需要在生成異常處理器代碼的函數棧中插入類似EXCEPTION_RECORD那樣的結構,在函數的調用棧中形成一個EXCEPTION_RECORD的鏈表。這個開銷對於異常或者非異常流程都是必須的,所以,對於非異常流程,可能沒有gcc的zero cost的方式有效,但是對於異常流程,就可以直接通過插入在棧中的EXCEPTION_RECORD的鏈表退棧,而無須像GCC一樣需要根據每一層調用函數的PC去遍歷每個模塊並搜索異常處理器。所以從效率上看,微軟的編譯器可能是兩者的平衡,在非異常的執行路徑,可能比gcc的代價大一點,但是在異常的路徑上,卻遠比gcc付出的代價要小。使用異常的另外一個代價是會有一定的空間的開銷,這個開銷一方面是使用異常的運行庫導致的,還有一個是編譯器需要為異常處理生成額外的代碼等。
在實際使用中,什麼時候該拋出異常,你可以基於這樣的假設去判斷:發生異常在程序執行過程中屬於低概率事件。至於需不需要catch,其實,很多時候寫catch僅僅是為了釋放無法自動釋放的資源,然後把異常拋出去,用智能指針,lock_guard等類似的方法可以去掉很多不必要的catch,而且有助於實現異常安全的代碼。使用異常+RAII(在不用RAII的情況下使用異常是作死異常僅當程序出現無法繼續進行的錯誤時才使用,勿濫用
已經有人回答了很多。我補充幾點。有些環境下異常處理不好要求完全控制流程和從錯誤中恢復錯誤的場景。這有點類似於0mq作者所說的。但我說的這種場景不一樣,我說的是系統已經很糟糕,各種鉤子各種註冊表或文件缺失,對,我說的就是安全軟體或者系統修復之類的場景。除此之外,普通程序員抵制異常通常是因為對異常缺乏理解。高級程序員抵制異常是因為擔心同事缺乏理解。在小團隊或個人項目中,我見到不少高級程序員大量的使用並且非常推崇異常。而同樣還是這個高級程序員,會在制定公司編碼規範時要求公司開發不使用異常。
錯誤有兩種。
一種是非運行時的,只能在開發階段出現的。這需要讓程序掛掉,解決掉問題才能繼續運行。程序真正運行的時候是不能有這種情況的。這裡就是用斷言的地方。判斷清楚在什麼時候使用斷言而不是返回錯誤提示很重要。一般我覺得,使用斷言的地方,就是沒有什麼好講的了,不應該不對,必須要對,錯了必須改的地方,或者你覺得錯了很low逼的問題。
一種是運行時的。這種錯誤是可預見的,是被邏輯判斷預先捕捉到的。比如資源錯誤,各種等待超時,數據錯誤等。這需要給出提示,然後能繼續運行就繼續,不能的就安全退出。這裡用異常捕捉或返回錯誤碼接著後續處理。
在使用異常捕捉或返回錯誤碼的時候,有個問題需要思考清楚。就是把問題就地處理掉,還是繼續拋到上層,交給函數調用者處理。有些情況,函數調用者有特殊的處理方式,異常拋出更好。有些情況在比較高內聚的模塊內的,功能實現和外界依賴關係小的,模塊內自己處理掉更好。Google C++ Style Guide都說不推薦使用C++ Exception。用返回值也是很方便的,你寫個宏嘛
#define TRY(func, args...)
do {
int __Ret__ = func(args);
if (__Ret__ != 0) {
return __Ret__;
}
} while (0)
用起來很方便的啊
可以把 Haskell 的 Maybe Error Monad 搬過來呀!(((
三種情況:1 動不動就使用異常,參數明明傳了個空指針,沒有攔截這明顯屬於編程規範的問題,不是異常,這是代碼不嚴謹或者是垃圾代碼。2 邏輯錯誤,程序沒有問題不會崩潰,但是不符合系統邏輯,要返回錯誤碼。3 運行時異常那才是真異常,能處理就處理掉,不能處理程序也運行不下去了。兩個極端,一種從來沒有異常,也不捕獲。另外一種,所有都是異常,程序要是所有異常就能搞定那編程不是容易得多,要什麼錯誤返回值管理?
我就問問,說用 assert的,難道只要有錯就退出整個程序運行?assert可是直接就exit咯
不僅是c++,只要語言支持,又沒有歷史包袱,就積極使用異常。
沒有異常的幫忙,連函數的連續調用都做不到,還能不能好好寫程序了。catch將每秒一幀的光追直接降至每秒0.03幀,還是在release下
推薦閱讀:
※相同的時間複雜度下,為什麼 C# 運行速度 比 C++ 快?
※用樹創建一個家譜,哪種表示法比較好?
※在C++編程實踐中,我們是否應該放棄使用realloc這個函數?
※初二學生能不能學C++?