處理錯誤就是取消操作

2021-10-05 05:40:23 字數 4688 閱讀 2343

實際上,我之前已經討論過這個話題了(這篇文章)。但鑑於我最近的經歷,我覺得需要重申一下,並進行一些調整。歸根結底,我所遇到的任何錯誤處理(error codes, errno, exceptions, error monad),都是在函式發生錯誤時,直接或間接取消它所依賴的操作。這對我們如何看待我們的程式流程,以及我們在應對程式中的錯誤時應該遵循哪些原則,都有一定的影響。

open_socket();

if (failed)

die();

resolve_host();

if (failed)

die();

connect();

if (failed)

die();

send_data();

if (failed)

die();

receive_data();

if (failed)

die();

此**實際上如下所示:

這反映了操作之間依賴性:

此依賴關係將進一步傳播到函式呼叫鏈的更高階別。這個玩具示例直接反映main函式的內部實現,它可以通過關閉應用程式來處理錯誤,同時也導致資源洩漏。但是一台本應執行數週或幾個月的商業伺服器,是承受不起的宕機的。在重要的程式中,我們必須關閉我們開啟的所有套接字(並清理所有其他資源),並報告全部的函式失敗,例如:

status get_data_from_server(hostname host)

如果任何操作失敗(以報告錯誤結尾),不僅函式內的後續操作被取消,而且整個函式報告失敗,並導致較高階別的下乙個函式的取消。我們的功能get_data_from_server()可在下列情況下呼叫:

status do_the_task()

這種級聯取消會繼續進行,因為我們不希望在依賴函式失敗時還呼叫它們:這樣做將是乙個錯誤。

這種操作依賴在幾乎每乙個程式中都是普遍存在的;在c++引入語法和特性時就反映了這種天生的依賴性:程式設計師不得不顯式地使用if語句。我們展開堆疊:輸入指令b在a的下面,已經表達了這種關係:「除非a成功,否則不要試圖呼叫b」。這程式**非常短,更清晰地描述了流程,如果它的依賴項失敗了,就不可能無視它而讓操作被呼叫。

取消依賴於失敗操作的操作:這是處理錯誤的核心。這就是返回**和if語句的情況,異常也是如此。在函式式程式語言中,error monads也表達了在失敗時跳過依賴操作(並傳播失敗)的概念。實際上,我們可以說c++異常處理機制是乙個內置於命令式語言中的(error monads):它跳過後續的操作,並把錯誤從throw處傳到異常處理catch處。如果出於某種原因不想用異常,還有很多供選擇的取消級聯操作的替代方案。例如,boost.outcome庫,函式返回型別可以表示成功的結果或關於失敗的資訊,而if語句隱藏在巨集後面,這些巨集能取消控制流。使用這個庫,函式do_the_task()將改為:

resultdo_the_task()

但是核心思想仍然是一樣的:如果它們所依賴的操作失敗了,就不要執行操作。我們可以稱之為取消級聯。與這一觀點有關的意見有很多。

您可能已經注意到了這一點,在第二個示例中get_data_from_server()函式的實現,我已經植入了乙個資源洩漏:因為任何操作的失敗,如果函式需要提前退出,則套接字將不會關閉。這種模式在**中也經常出現:如果獲取了資源,取消操作之前或之後,都必須釋放資源。換句話說,資源的釋放取決於資源的成功獲取,而不依賴於兩者之間的任何其他操作。為了在程式中反映這一點,我們需要乙個獲取資源的構造,如果這個獲取成功,則在資源不再使用的地方釋放資源,即使在取消級聯操作的情況下也要釋放。c++提供了一種解決方案:稱為raii,它就是這樣的語義。你需要以一種確定方式設計資源管理型別:建構函式獲取資源(如果它不工作則表示失敗),析構函式釋放資源。現在,當你在作用域中建立這樣的自動物件來管理資源時,我們得到了我們所需要的:

status get_data_from_server(hostname host)

; // opens the socket

if (failed) return failure();

resolve_host();

if (failed) return failure();

connect();

if (failed) return failure();

send_data();

if (failed) return failure();

receive_data();

if (failed) return failure();

return success();

} // closes the socket

注意:在本例中我們沒有使用異常。在任何取消操作的錯誤處理中,都存在著乙個問題:不應該被取消的操作被無意識地取消。這就是為什麼c程式設計師經常被告知,在乙個函式中只有乙個返回語句。但是對於raii來說,只有乙個返回語句的動機已經不再那麼強烈了。raii不僅用於堆疊展開,還可用於任何基於取消級聯的錯誤處理。

有了析構函式,你就可以將它用於任何事情,但是你必須遵守析構函式的指導方針:析構函式只用於釋放資源,許多其他事情就變得容易,明確和自然。獲取或釋放資源實際上從來不是函式的目標。函式的目標是產生某種價值或***,而資源只是實現目標的手段。示例中get_data_from_server()函式:它的目標是從伺服器獲取一些資料。它使用乙個套接字來獲取這些資料,但它只是乙個實現細節。如果套接字無法開啟,則無法從伺服器獲取資料:需要該資料的人現在必須被取消。同樣,如果從伺服器傳送或接收資料失敗,則需要取消資料的使用者。但是,如果我們從伺服器接收到資料並準備將其返回給使用者,但在此之前關閉套接字的嘗試失敗,就沒有必要啟動取消級聯:使用者將獲得他們所需的資料,隨後的函式將能夠完成他們的任務。我們可能洩露了資源,但這並不妨礙後續函式的繼續。稍後,資源的洩漏可能會導致其他操作的失敗,但那將是它的問題,就會開始它的取消級聯。

在異常處理上的乙個建議,在析構函式(用於釋放資源)即使由於某些原因而無法釋放資源,也不應該丟擲異常。這個建議可能會讓人不舒服:它看起來就像隱藏關於失敗的資訊。但是,必須注意:異常處理不是用於廣播任何系統中的故障;它是乙個工具,用於宣告操作之間的成功和失敗依賴關係,並控制取消級聯是如何進行的。如果不需要取消後續操作,則不要丟擲,可使用其他方式廣播有關故障的資訊,例如日誌記錄或某種全域性狀態。

大多數情況下,如果在源**中的操作b在操作a之後,這意味著b依賴於a的結果。如果a失敗了b就不能執行,否則我們將在沒有滿足先決條件的情況下呼叫b,這將是乙個錯誤。這意味著取消級聯不能在a和b之間。只有當b不取決於a的成功時,才能停止a和b之間的取消級聯。這種情況多久發生一次?答案是:在專案中的少數地方。例如,如果伺服器正在接收請求並在主迴圈中處理它們。即使乙個請求的處理失敗,伺服器也可以繼續處理下乙個請求。

另一種情況是某些函式需要返回一組記錄。它從三個伺服器獲取記錄:每個伺服器返回記錄的一部分。理想情況下,來自三颱伺服器的所有記錄都應該返回,但如果只有一台伺服器返回其記錄,則可以接受。因此,如果我們將從每個伺服器獲取資料的操作稱為a1,a2和a3,以及將記錄作為b,我們可以說,雖然有一些依賴性,b不依賴a1(在a1單獨失敗的情況下要取消),不依賴於a2,不依賴於a3。因此,我們期望在程式**中,開始於a1的取消級聯能在到達b之前停止。

當應用到異常處理機制時,我們會得到這樣的建議:不要捕獲異常,除非你確信後續操作不再依賴於try內的操作成功與否。我向你保證,在專案中這樣的情況不多。

通過取消級聯來處理錯誤意味著我們乙個接乙個地退出作用域,放棄操作並銷毀自動物件(呼叫它們的析構函式)。這就設定了一定的期望。對某個類物件的轉變操作可能會失敗,並可能使物件處於不希望的或其他意外狀態。這不是什麼大問題,因為失敗(在正確處理錯誤的程式中)將啟動取消級聯和涉及物件的後續操作將被取消。如果觀察到上一節中的規則,並且未過早停止級聯,則物件將超出作用域,再也不會被看到。然而,在此之前,仍然需要呼叫乙個操作:析構函式。因此,對物件的操作的設計提出了重要的要求:如果失敗,至少應該使物件處於可以「安全」銷毀的狀態(不引起ub、bug、資源洩漏)。(儘管如前所述,並將在以後的文章中討論,但有時我們可能無法防止資源洩漏。)這就是我們可以稱之為基本的故障安全保障。在c++環境中,它通常被稱為「基本的異常安全保障」,但應該指出:同樣適用於每個錯誤處理技術。我們期望每乙個變異操作都能提供這樣的保證,否則我們就不能在遇到故障時考慮程式的正確性。我們必須假設每乙個變異操作都滿足它。

事實上,儘管以上是類能執行的最低要求,但是基本的故障安全保證要求更多。儘管如此,還是有可能有人會在物件超出作用域之前停止取消級聯。在這種情況下,對句還存在,將有可能仍然使用它。在這種情況下,應該可以將物件重置到乙個很好理解的狀態。重置物件的最通用和最常見的方法是複製或為物件賦值乙個新值。因此,如果類提供拷貝或移動賦值函式,則對物件任何突變操作失敗了都是有保障的,物件不會引起ub、bug、資源洩漏。

其他型別可以提供重置其狀態的其他方法,例如,stl容器提供成員函式clear()。

事實上,基本的故障安全保證還需要一件事:狀態物件是有效,雖然沒有指定它可以是什麼特定的狀態,它可以是任何狀態只要是有效。但處於有效狀態意味著什麼呢?答案是:對於每種類型別,它的作者決定它處於有效狀態意味著什麼。最起碼的是,處於這種狀態的物件可以被銷毀或賦值(只要賦值運算子不被刪除),而不會導致ub、bug、資源洩漏。但是乙個類型別可以並且通常會保證更多:例如,它可以保證處於這種狀態的物件可以相互比較,或者它們可以被複製,或者它們上的每乙個操作都能工作,或者它們進入預設構造的狀態。但實際上,擔保的最後一部分並不能買到多少。在實踐中,與此相關的是,在發生故障後,可以安全地銷毀和重置物件。有了這一保證,取消級聯可以安全工作,而不會對程式造成損害。

今天就到此為止。總結一下建議:

UEFI下如何取消警告當成錯誤進行處理

在開發uefi的時候build的時候明明是個warning卻報錯,編譯不下去就需要下面的操作。pragma warning disable 4018 pragma warning disable 4090 pragma warning disable 4028 pragma warning disa...

段錯誤?打的就是段錯誤!!

呵,段錯誤?自從我看了這篇文章,我還會怕你個小小段錯誤?請開啟你的linux終端,跟緊咯,準備發車!嘟嘟嘟噠 include void errfunc intmain 這段 拿去執行,肯定段錯誤。系統會在程式崩潰的那一剎那將整個核心的資訊記錄在乙個檔案裡邊。如果你是第一次,那麼ls是查不到的。這樣 ...

Python錯誤處理操作示例

同j a一樣,在python中也有try.except.finaly的錯誤處理機制 try print try.r 5 0程式設計客棧 print result r except zerodivisionerror as e print except e finally print finally....