斷言、異常和返回值的選擇問題?
下面討論的是C++的情況,因為C++的異常比較雞肋,而且也比較複雜。。
比如一個函數,簡單的吧memcpy吧:void* memcpy(void *dest, const void *src, size_t n);在這種情況下,如果我要檢查src和dest是否為空指針,你們會怎樣做?這樣?assert(src!=NULL);
assert(dest != NULL); 還是這樣?throw std::invalid_argument("some things...");還是通過返回值處理?雖然谷歌的C++規範里不準使用異常,但是它又提到用返回值來做出錯處理是不太好的(具體忘了,大概是這個意思吧)。那麼實際應用中谷歌是怎麼處理這種情況的?另外,一些函數執行失敗返回錯誤代碼這種做法應該是比較合理的吧,但是在這種情況下,一個本來應該屬於這個函數的返回值就不得不出現在參數列表裡了。在這種情況下是用異常還是使用錯誤代碼?
要分清 返回false 和 斷言 和 異常 的區別:
依靠返回值返回錯誤的意思是 我有一個功能,這個功能很可能會失敗,例如:
File* FindFileWithName(FileList* list,const char* name); //當找不到時返回nullptr
文件列表中很可能沒有找到我想找的文件名,找不到這個文件是很正常的一種情況,所以通過返回值
確定.依靠異常的意思是,我有一個功能,這個功能99.999%的情況下都不會失敗,但是出於某種傻逼原因
它確實可能失敗,所以我弄個功能確保一下,例如:File* FindAppConfigurationFileInSystemDir(); //返回程序主配置文件,找不到就拋異常
函數形式跟上面那個函數差不多,但是這裡是拋異常,從道理上說這個主配置文件是一定存在的.(假設用戶安裝你軟體的時候這個文件已經被安裝到某個文件夾了).因為如果我連主程序的配置文件都找不到了,我程序還運行個屁啊,但是你確實無法保證某個用戶不會傻呵呵的刪掉這個文件.也就是說,異常的意思是"這個情況是正常的程序運行時不大可能出現的,但這個情況確實有很微小的可能會出現,基於良心上的譴責我做了個檢查." 大多數編譯器也是以這個原則來編譯拋異常舉動的的.例如當一個函數可能拋異常時,拋異常的代碼部分基本都會被編譯到最最不可能命中分支緩存的分支.
然後,斷言的意思是,按照我為我的代碼設定的邏輯,這種情況是絕對,永遠不可能出現,且不應該出現的.例如:
int size = myPriorityQueue.size();
myPriorityQueue.push(new QueueNode );ASSERT(myPriorityQueue.size()==size+1);這是一個優先隊列,我往隊列里加了一個元素並且排了序,那隊列的尺寸應該+1,總不可能我插入個元素再排個序隊列尺寸反倒-1吧.如果斷言失敗,說明這段代碼沒有正常工作,這不是程序問題而是代碼實現寫錯了.斷言的作用是檢查.拿你舉得例子,memcpy輸入了兩個nullptr怎麼辦? 當然是用斷言來檢查,因為這是不可能,不應該,不在程序設計計劃內的問題,要麼是你代碼寫錯了,要麼是使用你代碼的人用錯了你的代碼. 但是要注意,"不在你計劃內"的錯誤才能用斷言,如果的memcpy的文檔是這樣寫的:
void memcpy(...) //拷貝內存,如果你吃飽了撐的輸入nullptr那麼本函數將什麼都不幹直接返回如果是這樣,那顯然不需要加斷言.也就是說斷言主要是用來檢查你代碼是否按照你預期的目的執行,具體到這個問題,我會用assert,因為把NULL弄進memcpy肯定是程序員的錯,而不是用戶造成的。
檢查到程序員犯了錯誤,程序就應該立刻留下證據然後自殺,這樣才能逼程序員把代碼改好,無需妥協。
&> 一些函數執行失敗返回錯誤代碼這種做法應該是比較合理的吧,但是在這種情況下,一個本來應該屬於這個函數的返回值就不得不出現在參數列表裡了。
Google 對此的解決辦法是用 StatusOr&用法:
https://github.com/google/lmctfy/blob/master/include/lmctfy.h#L144當情況是bug的時候使用斷言,表明一定是要fix的,運行期不應該發生的。
針對採用return value 還是 exception,沒有一定的結論。Stackoverflow 有一個針對這個問題的討論 ( Which, and why, do you prefer Exceptions or Return codes? ) 不過基本上沒有結論,我比較喜歡的是拋出異常會強迫調用者處理,返回值則不會。無論那種,基本上都要有一堆的處理語句。斷言是一類,而返回值或者異常是另一類。前者是核查介面契約的手段,而後者本來就是介面契約的一部分。所以比較二者並不合適。該不該使用斷言完全卻決於介面的定義明確時是否需要進一步的核查,以便在契約違規時直接中斷,按照C傳統的說法,這個可以叫做行為為定義。一般的方式是直接終止程序,呼出調試器等。測試的工作目的之一就是保證儘可能所有的介面契約違規能夠被測出。
而返回值,異常,還有非局部跳轉都是介面的一部分。所以一個完整的介面定義不但包括輸入參數,返回值,還包括錯誤處理,以及可能的副作用。把錯誤信息包含在返回值的域中已經被證明問題多多,異常則是使用專門通道傳遞錯誤信息的辦法。
在異常使用方面,Google的C++規範是一個糟糕的代表。因為Google在程序員社區的強大影響力,非常多的C++程序員有意無意地拒絕使用異常。所以C++中的異常是被誤解最多的特性之一。異常的性能已經成倍提升,並不比任何傳統的錯誤處理機制更慢,但是在大型商業系統中的使用非常有限,使用好的,並且形成最佳實踐的更是少之又少。
非常多的甚至很優秀的C++商業程序在內存耗盡的時候直接選擇崩潰,而不是優雅地處理,這是C++程序員最大的羞恥。一個常見的解釋是反正內存耗盡什麼也幹不了不如直接重啟。殊不知,這樣使得程序的狀態完全不可控,而且無法有效組合擴展,這問題產生的後果恐怕遠比簡單重啟能解決的問題更嚴重。如果有必要在每次調用後檢查返回值,就用返回值。如果有必要包住所有調用,捕獲到異常並做恰當的處理,就用異常。不然,用斷言。
選擇的關鍵在於錯誤需要如何處理。
對於 memcpy 來說,如果 dst 或 src 為 NULL,更可能是代碼的問題,應該直接修改代碼而不是做某種處理,所以應該是用斷言。
自己的工程,直接assert。若是供其它人使用的庫,比較溫柔的是返回錯誤碼invalid parameters。
前條件不滿足用assert.簡單粗暴.
java編程思想中一段話:
感覺斷言很垃圾,知道錯了應該告訴調用者出錯了,需要處理,或者記錄當前的問題,FATAL日誌,然後主動退出,直接退出是個什麼意思呢,好多資源還沒回收了,junit的TestCase用斷言還行
推薦閱讀:
※Qt 為什麼在桌面應用(Windows 平台)中不流行呢?
※當面試官問我C++ 11新特性的時候,應該怎樣回答?
※既然c++模板元編程是圖靈完全的,那麼刷leetcode的時候如果都用模板元編程,能不能全刷成0ms?
※如何使用C++實現一個and函數?
※如何在 C++ 代碼中提示編譯器某個分支的執行概率高?
TAG:C |