天天看點

Google、Mozilla、Qt、LLVM 這幾家的規範是明确禁用異常的

作者:陳碩

連結:https://www.zhihu.com/question/22889420/answer/22975569

來源:知乎

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

整個 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<Derived&>(baseReference) 能報錯,因為沒有 null reference。還有一個原因是讓 overloaded operator 能報錯,畢竟 operator 的傳回類型往往無法包含 error code,例如 operator=() 傳回的是 Type&。C++ 也是唯一一個變量指派有可能會抛異常的語言,例如 Person s; s = getPersonById(someId);,那麼即便 getPersonById() 不抛異常也不能保證上一句指派不抛異常。)

(注腳2:C++ 引入異常的政治原因是 Ada 支援異常,而 Ada 是 DoD 的指定官方語言,如果 C++ 不支援異常,那麼 AT&T 貝爾實驗室就不能拿 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 即可。