第30章? 標準庫概觀(Standard-Library Overview)
目錄
30.1?? 引言
30.1.1?? 標準庫設施
30.1.2?? 設計約束
30.1.3?? 描述風格
30.2?? 頭文件
30.3?? 語言支持
30.3.1? ?對initializer_list的支持
30.3.2? ?對范圍for的支持
30.4?? 異常處理
30.4.1? ?異常
30.4.1.1? ?標準 exception的層級結構
30.4.1.2? ?異常的傳遞(Exception Propagation)
30.4.1.3? ?terminate() 函數
30.4.2? ?斷言(Assertions)
30.4.3? ?system_error
30.4.3.1? ?錯誤代碼
30.4.3.2? ?錯誤分類
30.4.3.3? ?異常 system_error
30.4.3.4? ?異常潛在的可移植錯誤條件(Potentially Portable Error Conditions)
30.4.3.5? ?映射錯誤代碼(Mapping Error Codes)
30.4.3.6? ?errc錯誤代碼
30.4.3.7? ?future_errc錯誤代碼
30.4.3.8? ?io_errc錯誤代碼
30.5?? 建議
30.1?? 引言
標準庫是 ISO C++ 標準指定的組件集,每個 C++ 實現都具有相同的行為(模數性能)。為了實現可移植性和長期可維護性,我強烈建議盡可能使用標準庫。也許你可以為你的應用程序設計和實現更好的替代方案,但是:
? 未來的維護者學習該替代設計有多容易?
? 十年后,該替代方案在未知平臺上可用的可能性有多大?
? 該替代方案對未來應用程序有用的可能性有多大?
? 你的替代方案與使用標準庫編寫的代碼互操作的可能性有多大?
? 你花費與標準庫一樣多的精力來優化和測試你的替代方案的可能性有多大?
當然,如果你使用了替代方案,你(或你的組織)將“永遠”負責該替代方案的維護和發展。總的來說:盡量不要重新發明輪子。
??? 標準庫相當大:ISO C++標準中的標準庫有785頁。這還沒有描述ISO C標準庫,它是C++標準庫的一部分(另外139頁)。相比之下,C++語言規范有398頁。在這里,我總結了一下,主要依靠表格,并給出了幾個例子。詳細信息可以在其他地方找到,包括標準的在線副本、完整的在線實現文檔,以及(如果你喜歡閱讀代碼)開源實現。完整的詳細信息請參閱對標準的引用。
??? 標準庫章節不宜按其呈現順序閱讀。每個章節和每個主要小節通常都可以單獨閱讀。如果遇到不明之處,請依靠交叉引用和索引。
30.1.1?? 標準庫設施
??? 標準 C++ 庫中應該包含什么?一種理想情況是,程序員能夠在庫中找到每個有趣、重要且相當通用的類、函數、模板等。然而,這里的問題不是“某個庫中應該包含什么?”而是“標準庫中應該包含什么?”“一切!”是對前一個問題的合理初步近似回答,但不是后一個問題。標準庫是每個實現者都必須提供的東西,以便每個程序員都可以依賴它。
??? C++ 標準庫提供:
? 支持語言功能,例如內存管理(§11.2)、范圍for 語句
(§9.5.1)和運行時類型信息(§22.2)
? 有關語言實現定義方面的信息,例如最大有限浮點值(§40.2)
? 語言本身無法輕松或高效實現的原操作,例如 is_polymorphic、is_scalar 和 is_nothrow_constructible(§35.4.1)
? 底層(“無鎖”)并發編程設施(§41.3)
? 支持基于線程的并發(§5.3,§42.2)
? 對基于任務的并發的最低限度支持,例如 Future 和 async()(§42.4)
? 大多數程序員無法輕松實現最佳和可移植的函數,例如 uninitialized_fill()(§32.5)和 memmove()(§43.5)
?對未使用內存回收(垃圾收集)的最低限度支持(可選),例如 declare_reachable()(§34.5)
? 程序員可以依賴的非原基礎設施,以實現可移植性,例如list(§31.4)、map(§31.4.3)、sort()(§32.6)和 I/O 流(第 38 章)
? 用于擴展其提供的設施的框架,例如約定和支持設施允許用戶以內置類型的 I/O(第 38 章)和 STL(第 31 章)的樣式提供用戶定義類型的 I/O 。
標準庫提供的一些功能僅僅是因為這樣做很常規而且有用。例如標準數學函數,如 sqrt() (§40.3)、隨機數生成器 (§40.7)、complex算術 (§40.4) 和正則表達式 (第 37 章)。
??? 標準庫旨在成為其他庫的共同基礎。具體來說,其功能的組合使標準庫能夠發揮三種支持作用:
? 可移植性的基礎
? 一組緊湊而高效的組件,可用作性能敏感型庫和應用程序的基礎
? 一組支持庫內通信的組件
??? 庫的設計主要由這三個角色決定。這些角色密切相關。例如,可移植性通常是專用庫的重要設計標準,而list和map等常見容器類型對于單獨開發的庫之間的便捷通信至關重要。
??? 從設計角度來看,最后一個角色尤其重要,因為它有助于限制標準庫的范圍并限制其功能。例如,標準庫中提供了字符串和列表功能。如果沒有,單獨開發的庫只能通過使用內置類型進行通信。但是,沒有提供高級線性代數和圖形功能。這些功能顯然用途廣泛,但它們很少直接參與單獨開發的庫之間的通信。
除非需要某種工具來支持這些角色,否則可以將其留給標準之外的某個庫。無論好壞,將某些東西排除在標準庫之外都會為不同的庫提供機會,讓它們提供想法的競爭性實現。一旦某個庫證明自己在各種計算環境和應用領域中廣泛有用,它就會成為標準庫的候選。正則表達式庫(第 37 章)就是一個例子。
精簡的標準庫可用于獨立實現,即在最少或沒有操作系統支持的情況下運行的實現(§6.1.1)。
30.1.2?? 設計約束
??? 標準庫的角色對其設計施加了一些約束。C++ 標準庫提供的功能旨在:
? 對幾乎每一位學生和專業程序員(包括其他庫的構建者)來說都是有價值且負擔得起的。
? 每位程序員都可以直接或間接地使用該庫范圍內的所有內容。
? 足夠高效,可以在進一步實現庫時提供手工編碼函數、類和模板的真正替代方案。
? 要么不包含策略,要么可以選擇將策略作為參數提供。
? 從數學意義上講是原始的。也就是說,與設計為僅執行單一角色的單個組件相比,服務于兩個弱相關角色的組件幾乎肯定會承受開銷。
? 對于常見用途來說,方便、高效且相當安全。
? 功能齊全。標準庫可能會將主要功能留給其他庫,但如果它承擔一項任務,它必須提供足夠的功能,以便單個用戶或實施者無需替換它即可完成基本工作。
? 易于使用,具有內置類型和操作。
? 默認情況下類型安全,因此原則上可以在運行時檢查。
? 支持普遍接受的編程風格。
? 可擴展以處理用戶定義類型,處理方式類似于處理內置類型和標準庫類型。
??? 例如,將比較標準構建到排序函數中是不可接受的,因為相同的數據可以根據不同的標準進行排序。這就是為什么 C 標準庫 qsort() 將比較函數作為參數,而不是依賴于某些固定的東西,例如 < 運算符(§12.5)。另一方面,每次比較的函數調用所帶來的開銷損害了qsort() 作為進一步構建庫的構建塊。對于幾乎所有數據類型,都很容易進行比較,而無需施加函數調用的開銷。
??? 這種開銷嚴重嗎?在大多數情況下,可能不是。但是,函數調用開銷可能會占據某些算法的執行時間,并導致用戶尋求替代方案。§25.2.3 中描述的通過模板參數提供比較標準的技術解決了 sort() 和許多其他標準庫算法的這個問題。sort 示例說明了效率和通用性之間的矛盾。它也是解決這種矛盾的一個例子。標準庫不僅需要執行其任務。它還必須如此高效地執行這些任務,以至于用戶不會傾向于提供標準提供的替代方案。否則,更高級功能的實現者將被迫繞過標準庫以保持競爭力。這會給庫開發人員增加負擔,并嚴重復雜化希望保持平臺獨立性或使用多個單獨開發的庫的用戶的生活。
??? “原始性(primitiveness)”和“常見用途的便利性”的要求可能會發生沖突。前者要求不允許專門針對常見情況優化標準庫。但是,除了原設施之外,標準庫中還可以包含滿足常見但非原需求的組件,而不是作為替代品。正交性狂熱者不應妨礙我們為新手和普通用戶提供便利。它也不應導致我們讓組件的默認行為變得模糊或危險。
30.1.3?? 描述風格
即使是一個簡單的標準庫操作(例如構造函數或算法)的完整描述也可能需要幾頁紙。因此,我使用了一種極其簡潔的演示風格。相關操作集通常以表格形式呈現:
某些操作 | |
p=op(b,e,x) | op對范圍 [b,e)和x做某事,返回給p |
foo(x) | foo對x做某事,但不返回結果 |
bar(b,e,x) | x 和 ?[b,e) 有關系嗎? |
在選擇標識符時,我盡量做到易記,因此 b 和 e 將是指定范圍的迭代器,p 是指針或迭代器,x 是某個值,具體含義取決于上下文。在這種表示法中,只有注釋無法區分結果和布爾值結果,所以如果你用力過猛,很容易混淆它們。對于返回布爾值的操作,解釋通常以問號結尾。如果算法遵循通常的模式,返回輸入序列的結尾來指示“失敗”、“未找到”等(§4.5.1,§33.1.1),我就不會明確提及這一點。
??? 通常,這種簡短的描述會附帶 ISO C++ 標準的參考、一些進一步的解釋和示例。
30.2?? 頭文件
??? 標準庫的功能定義在 std 命名空間中,并以一組頭文件的形式呈現。這些頭文件標識了庫的主要部分。因此,列出這些頭文件可以概覽庫的構成。
??? 本小節的其余部分是按功能分組的頭文件列表,并附有簡短說明,并附有討論這些頭文件的參考文獻。分組方式的選擇與標準的組織結構相符。
??? 名稱以字母 c 開頭的標準頭文件相當于 C 標準庫中的頭文件。對于全局命名空間和命名空間 std 中每一個定義 C 標準庫部分的頭文件 <X.h>(譯注:即在C中定義的標準庫頭文件,在C++環境中加前綴c),都有一個定義相同名稱的頭文件 <cX>。理想情況下,<cX> 頭文件中的名稱不會污染全局命名空間(§15.2.4),但遺憾的是(由于維護多語言、多操作系統環境的復雜性),大多數頭文件都會污染全局命名空間。
容器 | ||
<vector> | 一維伸縮數組 | §31.4.2 |
<deque> | 雙端隊列 | §31.4.2 |
<forward_list> | 單向鏈表 | §31.4.2 |
<list> | 雙向鏈表 | §31.4.2 |
<map> | 關聯數組 | §31.4.3 |
<set> ?? | 集合 | §31.4.3 |
<unordered_map> | 哈希關聯數組 | §31.4.3.2 |
<unordered_set> | 哈希集合 | §31.4.3.2 |
<queue> | 隊列 | §31.5.2 |
<stanck> | 棧 | §31.5.1 |
<array> | 一維定長數組 | §34.2.1 |
<bitset>(位集) | bool數組 | §34.2.2 |
關聯容器 multimap 和 multiset 分別在 <map> 和 <set> 中聲明。priority_queue (§31.5.3) 在 <queue> 中聲明。
通用工具 | ||
<utility> | 運算符和對(pairs) | §35.5, §34.2.4.1 |
<tuple> | 三元組 | §34.2.4.2 |
<type_traits> | 類型trait | §35.4.1 |
<typeindex> | 使用一個 type_info 作為key或哈希 code | §35.5.4 |
<functional> | 函數對象 | §33.4 |
<memory> | 資源管理指針 | §33.3 |
<scoped_allocator> | 作用域分配器 | §34.4.4 |
?<ratio> | 編譯時比率(有理)運算 | §35.3 |
<chrono> | 時間工具 | §33.2 |
<ctime> | C風格時期和時間 | §43.6 |
<iterator> | 迭代器和迭代器支持 | §33.1 |
迭代器提供了使標準算法通用的機制(§3.4.2,§33.1.4)。
算法 | ||
<algorithm> | 通用算法 | §32.2 |
?<cstdlib> | bsearch(), qsort() | §43.7 |
典型的通用算法可以應用于任意元素類型的任意序列(§3.4.2,§32.2)。C 標準庫函數 bsearch() 和 qsort() 僅適用于元素類型不包含用戶定義復制構造函數和析構函數的內置數組(§12.5)。
算法 | ||
<exception> | 異常類 | §30.4.1.1 |
?<stdexcept> | 標準異常 | §30.4.1.1 |
<cassert> | 斷言宏 | §30.4.2 |
<cerrno> | C風格錯誤處理 | §13.1.2 |
<system_error> | 系統錯誤支持 | §30.4.3 |
使用異常的斷言在§13.4 中描述。
字符串和字符 | ||
<string> | T字符串 | 第36章 |
?<cctype> | 字符分類 | §36.2.1 |
<cwctype> | 寬字符分類 | §36.2.1 |
<cstring> | C風格字符串函數 | §43.4 |
<cwchar> | C風格寬字符串函數 | §36.2.1 |
<cstdlib> | C風格分配函數 | §43.5 |
<cuchar> | C風格多字符 | |
<regex> | 正則表達匹配 | 第37章 |
<cstring> 頭文件聲明了 strlen() ,strcpy() 等函數系列。<cstdlib> 聲明了 atof() 和 atoi(),它們可以將 C 風格的字符串轉換為數值。
輸入/輸出 | ||
<iosfwd> | I/O 設施的前向聲明 | §38.1 |
?<iostream> | 標準iostream對象和操作 | §38.1 |
<ios> | Iostream基類 | §38.4.4 |
<streambuf> | 流緩沖區???????????????? | §38.6 |
<istream> | 輸入流模板 | §38.4.1 |
<ostream> | 輸出流模板 | §38.4.2 |
<iomanip> | 操縱器 | §38.4.5.2 |
<sstream> | 至/源自字符串的流 | §38.2.2 |
<cctype> | 字符分類函數 | §36.2.1 |
<fstream> | 至/源自文件的流 | §38.2.1 |
<cstdio> | I/O的printf族 | §43.3 |
<cwchar> | 寬字符的I/O的printf類型 | §43.3 |
操縱器是用于操縱流狀態的對象(§38.4.5.2)。
本土化 | ||
<locale> | 表示文化差異 | 第37章 |
<clocale> | 表示C風格的文化差異 | §43.7 |
<codecvt> | 代碼約定facet | §39.4.6 |
一個locale設置本地化差異,例如日期的輸出格式、用于表示貨幣的符號以及不同自然語言和文化之間不同的字符串排序標準。
語言支持 | ||
<limits> | 數的極限 | §40.2 |
<climits> | 表示C風格的文化差異 | §40.2 |
<cfloat> | C風格數值標量極限宏 | §40.2 |
<cstdint> | 標準整數類型名 | §43.7 |
<new> | 動態內存管理 | §11.2.3 |
<typeinfo> | 運行時類型識別支持 | §22.5 |
<exception> | 異常處理支持 | §30.4.1.1 |
<initializer_list> | initializ er_list | §30.3.1 |
<cstddef> | C庫語言支持 | §10.3.1 |
<cstdarg> | 可變長度函數參數列表 | §12.2.4 |
<csetjmp> | C 風格堆棧展開 | |
<cstdlib> | 程序中止 | §15.4.3 |
<ctime> | 系統時鐘 | §43.6 |
<csignal> | C 風格信號處理 |
<cstddef> 頭文件定義了 sizeof() 返回值的類型 size_t、指針減法結果和數組下標的類型 ptrdiff_t (§10.3.1),以及臭名昭著的 NULL 宏(§7.2.2)。
??? C 風格的堆棧展開(使用 <csetjmp> 中的 setjmp 和 longjmp)與析構函數的使用以及異常處理(第 13 章 §30.4)不兼容,最好避免使用。本書不討論 C 風格的堆棧展開和信號。
數值 | ||
<complex> | 復數及其相關操作 | §40.4 |
<valarray> | 數值向量及其相關操作 | §40.5 |
<numeric> | 廣義數值操作 | §40.6 |
<cmath> | 標準數學函數 | §40.3 |
<cstdlib> | C風格隨機數 | §40.7 |
<random> | 隨機數生成器 | §40.7 |
由于歷史原因,abs() 和 div() 位于 <cstdlib> 中,而不是與其余數學函數一起位于 <cmath> 中(§40.3)。
并發 | ||
<atomic> | 原子類型及其相關操作 | §41.3 |
<condition_variable> | 條件變量(等待一個操作) | §42.3.4 |
<future> | 異步任務 | §42.4.4 |
<mutex> | 互斥類 | §42.3.1 |
<thread> | 線程相關操作 | §42.2 |
C 語言為 C++ 程序員提供了各種相關的標準庫功能。C++ 標準庫提供了對以下所有功能的訪問:
對C的兼容性 | |
<cinttypes> | 通用整數類型的別名 |
<cstdbool> | C bool |
<ccomplex> | <complex> |
<cfenv> | 浮點環境 |
<cstdalign> | C字節對齊 |
<ctgmath> | C “類型泛型數學”:<complex>和<cmath> |
<cstdbool> 頭文件不會定義宏 bool,true 或 false。<cstdalign> 頭文件不會定義宏 alignas。<cstdbool>, <ccomplex>, <calign> 和 <ctgmath> 的 .h 等效文件類似于 C++ 的 C 功能。請盡量避免使用它們。
??? <cfenv> 頭文件提供類型(例如 fenv_t 和 fexcept_t),浮點狀態標志和描述實現的浮點環境的控制模式。
??? 用戶或庫實現者不得在標準頭文件中添加或刪除聲明。也不允許通過定義宏來更改頭文件的內容,從而改變頭文件中聲明的含義(§15.2.3)。任何玩弄此類把戲的程序或實現都不符合標準,依賴此類技巧的程序不可移植。即使它們現在能夠正常工作,實現中任何部分的下一個版本都可能破壞它們。請避免此類伎倆。
??? 要使用標準庫工具,必須包含其頭文件。自己編寫相關聲明并非符合標準的做法。原因是,某些實現會根據標準頭文件的包含來優化編譯,而另一些實現則會提供由頭文件觸發的標準庫工具的優化實現。通常,實現者會以程序員無法預測且不應知曉的方式使用標準頭文件。
??? 然而,程序員可以專門為非標準庫、用戶定義類型設計實用程序模板,例如 swap() (§35.5.2)。
30.3?? 語言支持
??? 標準庫的一個小但必不可少的部分是語言支持,即程序運行必須具備的功能,因為語言特性依賴于它們。
庫支持的語言特征 | ||
<new> | new和delete | §11.2 |
<typeinfo> | typeid()和type_info | §22.5 |
<iterator> | 范圍for | §30.3.2 |
<initializer_list> | initializer_list | §30.3.1 |
30.3.1? ?對initializer_list的支持
??? 根據§11.3中描述的規則,{} 列表 會被轉換為 std::initializer_list<X> 類型的對象。在 <initializer_list> 中,我們找到 initializer_list:
template<typename T>
class initializer_list { // §iso.18.9
public:
using value_type = T;
using reference = const T&; // 注意 const:initializer_list 元素是不可變的
using const_reference = const T&;
using size_type = size_t;
using iterator = const T?;
using const_iterator = const T?;
initializer_list() noexcept;
size_t siz e() const noexcept; // number of elements
const T? begin() const noexcept; // first element
const T? end() const noexcept; // one-past-last element
};
template<typename T>
const T? begin(initializer_list<T> lst) noexcept { return lst.begin(); }
template<typename T>
const T? end(initializer_list<T> lst) noexcept { return lst.end(); }
??? 遺憾的是,initializer_list 不提供下標運算符。如果要使用 [] 而不是 ?,請對指針取下標:
void f(initializer_list<int> lst)
{
for(int i=0; i<lst.size(); ++i)
cout << lst[i] << '\n'; // error
const int? p = lst.begin();
for(int i=0; i<lst.size(); ++i)
cout << p[i] << '\n'; // OK
}
當然,initializer_list 也可以用于范圍for 語句。例如:
void f2(initializer_list<int> lst)
{
for (auto x : lst)
cout << x << '\n';
}
30.3.2? ?對范圍for的支持
?????? 按照§9.5.1 中的描述,使用迭代器將范圍for 語句映射到 for 語句。
??? 在 <iterator> 中,標準庫為內置數組和提供成員 begin() 和 end() 的每種類型提供了 std::begin() 和 std::end() 函數;參見 §33.3。
??? 所有標準庫容器(例如,vector 和 unordered_map)和字符串都支持使用范圍for 進行迭代;容器適配器(例如,stack 和 prioritize_queue)則不支持。容器頭文件(例如,<vector>)包含 <initializer_list>,因此用戶很少需要直接執行此操作。
30.4?? 異常處理
??? 標準庫包含近 40 年來開發的組件。因此,它們的風格和錯誤處理方法并不一致:
? C 語言庫包含許多函數,其中許多函數會設置 errno 來指示發生了錯誤;參見 §13.1.2 和 §40.3。
? 許多對元素序列進行操作的算法會返回一個指向倒數第二個元素的迭代器,以指示“未找到”或“失敗”;參見 §33.1.1。
? I/O 流庫依賴于每個流中的狀態來反映錯誤,并且可能(如果用戶要求)拋出異常來指示錯誤;參見 §38.3。
? 一些標準庫組件(例如 vector,string 和 bitset)會拋出異常來指示錯誤。
標準庫的設計使得所有功能都遵循“基本保證”(§13.2);也就是說,即使拋出異常,也不會泄漏任何資源(例如內存),也不會破壞標準庫類的不變量。
30.4.1? ?異常
??? 一些標準庫工具通過拋出異常來報告錯誤:
標準庫異常 | |
bitset | 拋出異常 invalid_argument, out_of_range, overflow_error |
iostream | 若開啟了異常則會拋出異常 ios_base::failure |
regex | 拋出異常regex_error |
string | 拋出異常length_error, out_of_range |
vector | 拋出異常out_of_range |
new T | 若不能為T分配內存則拋出異常bad_alloc |
dynamic_cast<T>(r) | 若不能將一個引用r類型轉換為一個T ,則拋出異常bad_cast |
typeid() | 若不能提供一個type_info則拋出異常bad_typeid |
thread | 拋出異常system_error |
call_once() | 拋出異常system_error |
mutex | 拋出異常system_error |
condition_variable | 拋出異常system_error |
async() | 拋出異常system_error |
packaged_task | 拋出異常system_error |
future 和 promise | 拋出異常future _error |
??? 任何直接或間接使用這些功能的代碼都可能遇到這些異常。此外,任何操作可能引發異常的對象的操作都必須被假定會引發該異常,除非已采取預防措施。例如,如果 packaged_task 需要執行的函數引發了異常,它也會引發異常。
??? 除非您知道任何工具的使用方式都可能引發異常,否則最好始終在某個地方(§13.5.2.3)捕獲標準庫異常層次結構的根類之一(例如exception)以及任何異常(...),例如在 main() 中。
30.4.1.1? ?標準 exception的層級結構
??? 不要拋出內置類型,例如 int 和 C 語言風格的字符串。相反,應該拋出專門定義為異常類型的對象。
??? 標準異常類的層級結構提供了異常的分類:
此層級結構旨在為標準庫定義的異常提供一個框架。邏輯錯誤原則上可以在程序開始執行之前捕獲,也可以通過函數和構造函數的參數測試捕獲。運行時錯誤是所有其他錯誤。system_error 在§30.4.3.3中描述。
??? 標準庫異常層級結構以 exception 為基類:
class exception {
public:
exception();
exception(const exception&);
exception& operator=(const exception&);
virtual ?exception();
virtual const char? what() const;
};
what() 函數可用于獲取一個字符串,該字符串應該指示有關導致異常的錯誤的信息。
?????? 程序員可以通過從標準庫異常類派生來定義異常,如下所示:
struct My_error : runtime_error {
My_error(int x) :runtime_error{"My_error"}, interesting_value{x} { }
int interesting_value;
};
?????? 并非所有異常都屬于標準庫異常層級結構。但是,標準庫拋出的所有異常都來自該異常層次結構。
除非你知道任何工具的使用方式都可能引發異常,否則最好在某個地方捕獲所有異常。例如:
int main()
try {
// ...
}
catch (My_error& me) { //? My_error 異常發生
// 我們可以使用 me.interesting_value 和me.what()
}
catch (runtime_error& re) { // runtine_error 異常發生
// 我們可以使用 re.what()
}
catch (exception& e) { // 某個標準庫異常發生
// 我們可以使用 e.what()
}
catch (...) { // 某個前面沒有捕捉到的異常發生
// 我們可以做局部清理
}
對于函數參數,我們使用引用來避免分片(§17.5.1.4)。
30.4.1.2? ?異常的傳遞(Exception Propagation)
在 <exception> 中,標準庫提供了使程序員可以訪問異常傳遞的功能:
某些操作 | |
exception_ptr | 用于指向異常的未指定類型 |
ep=current_exception() | ep 是一個指向當前異常的 exception_ptr,如果當前沒有活動異常,則指向沒有異常;函數聲明為noexcept |
rethrow_exception(ep) | 重新拋出由ep所指向的異常 |
ep=make_exception_ptr(e) | ep 所包含的指針不能為 nullptr;noreturn (§12.1.7) ep=make_exception_ptr(e) ep 是指向異常 e 的 exception_ptr;函數聲明為 noexcept |
exception_ptr 可以指向任何異常,而不僅僅是異常層級結構中的異常。可以將 exception_ptr 視為一個智能指針(類似于 shared_ptr),只要 exception_ptr 指向它,它就會保持異常有效。這樣,我們可以將 exception_pointer 傳遞給捕獲異常的函數之外的異常,并在其他地方重新拋出。具體來說,exception_ptr 可用于實現在與捕獲異常的線程不同的其它線程中重新拋出異常。這正是 promise 和 future(§42.4)所依賴的。在 exception_ptr 上使用 rethrow_exception()(來自不同的線程)不會引發數據競爭。
??? make_exception_ptr() 可以實現為:
template<typename E>
exception_ptr make_exception_ptr(E e) noexcept;
try {
throw e;
}
catch(...) {
return current_exception();
}
nested_exception 是一個存儲了通過調用 current_exception() 獲得的 exception_ptr 的類:
nested_exception (§iso.18.8.6) | |
nested_exception ne {}; | 默認構造函數:ne存儲一個指向 current_exception() 的 exception_ptr 指針;標為 noexcept 。 |
nested_exception ne {ne2}; | 復制構造函數:ne和ne2分別存儲一個指向存儲異常的exception_ptr指針。 |
ne2=ne | 復制賦值:ne和ne2分別存儲一個指向存儲異常的exception_ptr指針。 |
ne.?nested_exception() | 析構函數; 聲明為virtual 。 |
ne.rethrow_nested() | 重新拋出ne存儲的異常;如果其中沒有存儲異常,則調用 terminate();此函數聲明為noreturn。 |
ep=ne.nested_ptr() | ep 是一個 exception_ptr指針,指向 ne 存儲的異常;聲明為noexcept 。 |
throw_with_nested(e) | 拋出一個基類型派生于 nested_exception 的異常以及e的類型的異常,e 不能從 nested_exception 派生;聲明為 noreturn 。 |
rethrow_if_nested(e) | dynamic_cast<const nested_exception&>(e).rethrow_nested(); e 的類型必須派生自 nested_exception 。 |
nested_exception 的預期用途是作為異常處理程序使用的類的基類,用于將一些關于錯誤的本地上下文的信息以及一個 exception_ptr 指針傳遞給導致調用它的異常。例如:
struct My_error : runtime_error {
My_error(const string&);
// ...
};
void my_code()
{
try {
// ...
}
catch (...) {
My_error err {"something went wrong in my_code()"};
// ...
throw_with_nested(err);
}
}
現在,My_error 信息與包含指向捕獲到的異常的 exception_ptr 的 nested_exception 一起傳遞(重新拋出)。
??? 沿著調用鏈往上走,我們可能需要查看嵌套異常:
void user()
{
try {
my_code();
}
catch(My_error& err) {
// ... clear up My_error problems ...
try {
rethrow_if_nested(err); // 重新拋出嵌套異常(如果有的話)
}
catch (Some_error& err2) {
// ... 清除 Some_error 問題 ...
}
}
}
?????? 這假設我們知道 some_error 可能與 My_error 嵌套。
?????? 異常不能從 noexcept 函數中傳遞出去(§13.5.1.1)。
30.4.1.3? ?terminate() 函數
?????? 在 <exception> 中,標準庫提供了處理意外異常的功能:
terminate() (§iso.18.8.3, §iso.18.8.4) | |
h=get_terminate() | h是當前終止句柄;聲明為 noexcept。 |
h2=set_terminate(h) | h 成為當前終止句柄。是h2 前一個終止句柄。聲明為 noexcept 。 |
terminate() | 終止當前程序;聲明為 noreturn,noexcept |
uncaught_exception() | 當前線程是否拋出了異常,但尚未捕獲?聲明為noexcept 。 |
避免使用這些函數,除非極少數情況下使用 set_terminate() 和 terminate()。調用terminate()會通過調用由 set_terminate() 設置的終止處理程序來終止程序。默認設置(幾乎總是正確的)是立即終止程序。出于操作系統的基本原因,在調用terminate() 時是否調用本地對象的析構函數由實現定義。如果由于 noexcept 違規而調用terminate(),則系統允許進行(重要的)優化,這意味著堆棧甚至可能被部分展開(§iso.15.5.1)。
??? 有時人們聲稱 uncaught_exception() 可以用來編寫析構函數,使其根據函數是正常退出還是異常退出而行為不同。然而,在捕獲初始異常后,在堆棧展開(§13.5.1)期間,uncaught_exception() 也同樣有效。我認為 uncaught_exception() 過于隱晦,不適合實際使用。
30.4.2? ?斷言(Assertions)
??? 標準庫提供了斷言:
斷言(§iso.7) | |
static_assert(e,s) | 編譯時計算e ;若 !e 為真,則給出s為編譯器錯誤消息。 |
assert(e) | 若宏 NDBUG 未定義,在運行時計算e ,而若!e 為真,向 cerr 寫入一條信息并調用 abort();若NDBUG 已定義,則不做任何操作。 |
例如:
template<typename T>
void draw_all(vector<T?>& v)
{
static_assert(Is_base_of<Shape,T>(),"non?Shape type for draw_all()");
for (auto p : v) {
assert(p!=nullptr);
// ...
}
}
assert() 是 <cassert> 中的一個宏。assert() 生成的錯誤消息由實現定義,但應包含源文件名 (__FILE__) 以及包含 assert() 的源代碼行號 (__LINE__)。
??? 斷言在生產代碼中的使用頻率比在小型說明性教科書中的示例中更高(正如它們應該的那樣)。
??? 函數名稱 (__func__) 也可能包含在消息中。如果假設 assert() 會被求值,而實際上并沒有,那么這可能是一個嚴重的錯誤。例如,在通常的編譯器設置下,assert(p!=nullptr) ?會在調試期間捕獲錯誤,但在最終發布的產品中不會捕獲錯誤。
??? 對于管理斷言的方法,請參閱§13.4。
30.4.3? ?system_error
??? 在 <system_error> 中,標準庫提供了一個用于報告來自操作系統和底層系統組件的錯誤框架。例如,我們可以編寫一個函數來檢查文件名,然后像這樣打開一個文件:
ostream& open_file(const string& path)
{
auto dn = split_into_directory_and_name(path); // 拆分成 {path,name}
error_code err {does_directory_exist(dn.first)}; //詢問 "系統" 有關路徑事項
if (err) { // err!=0 意味著錯誤
// ... 看看可否做某事 ...
if (cannot_handle_err)
throw system_error(err);
}
// ...
return ofstream{path};
}
假設“系統”不知道 C++ 異常,我們就別無選擇是否處理錯誤代碼;唯一的問題是“在哪里?”和“如何處理?”。在 <system_error> 中,標準庫提供了對錯誤代碼進行分類、將系統特定的錯誤代碼映射到更可移植的錯誤代碼以及將錯誤代碼映射到異常的功能:
系統錯誤類型 | |
error_code | 保存一個標識錯誤和錯誤類別的值;系統特定(§30.4.3.1)。 |
error_category | 用于識別特定類型(類別)錯誤代碼的來源和編碼的類型的基類(§30.4.3.2)。 |
system_error | 包含 error_code 的運行時錯誤異常(§30.4.3.3)。 |
error_condition | 保存一個標識錯誤和錯誤類別的值;可能具有可移植性(§30.4.3.4)。 |
errc | enum class,包含來自 <cerrno> (§40.3) 的錯誤代碼枚舉器;基本為 POSIX 錯誤代碼。 |
future_errc | 帶有來自 <future> 的錯誤代碼枚舉器的enum class (§42.4.4)。 |
io_errc | 帶有來自 <ios> 的錯誤代碼枚舉器的enum class (§38.4.4)。 |
30.4.3.1? ?錯誤代碼
當錯誤以錯誤代碼的形式從較低層級“冒泡”時,我們必須處理它所代表的錯誤,或者將其轉換為異常。但首先我們必須對其進行分類:不同的系統對同一問題使用不同的錯誤代碼,而不同的系統只是存在不同類型的錯誤。
error_code (§iso.19.5.2) | |
error_code ec {}; | 默認構造函數:ec={0,&generic_category}; 聲明為 noexcept 。 |
error_code ec {n,cat}; | ec={n,cat};? cat 是一個 error_category類型;并且 n 是一個 int,表示 cat 中的錯誤;noexcept 。 |
error_code ec {n}; | n 表示錯誤;n 是 EE 類型的值,其 is_error_code_enum<EE>::value==true;聲明為noexcept。 |
ec.assign(n,cat) | ec={and,cat}; cat 是一個錯誤類別;n 表示錯誤;n 是 EE 類型的值,且 is_error_code_enum<EE>::value==true;聲明為noexcept 。 |
ec=n | ec={n,&generic_category}:ec=make_error_code(n); n 表示錯誤;n 是 EE 類型的值,其中 is_error_code_enum<EE>::value==true;聲明為noexcept 。 |
ec.clear() | ec={0,&generic_categor y()}; 聲明為noexcept 。 |
n=ec.value() | n是 ec 的存儲值;聲明為noexcept 。 |
cat=ec.category() | cat 是對 ec 存儲類別的引用;聲明為noexcept 。 |
s=ec.message() | sis 表示 ec 的字符串,可能用作錯誤消息:ec.category().message(ec.value()) 。 |
bool b {ec}; | 將 ec 轉換為 bool;如果 ec 表示錯誤,則 b 為 true;也就是說,b==false 表示“無錯誤”;聲明為explicit 。 |
ec==ec2 | ec 和 ec2 中的一個或兩個都可以是 error_code;要比較相等,ec 和 ec2 必須具有等效的 category() 和等效的值;如果 ec 和 ec2 屬于同一類型,則等效性由 == 定義;如果不是,則等效性由 category().equivalent() 定義。 |
ec!=ec2 | !(ec==ec2) |
ec<ec2 | 順序 ec.category()<ec2.category() || (ec.category()==ec2.category() && ec.value()<ec2.value()) |
e=ec.default_error_condition() | e 是對 error_condition 的引用: e=ec.category().default_error_condition(ec.value()) 。 |
os<<ec | 將 ec.name() 寫入 ostream os |
ec=make_error_code(e) | e 是一個錯誤; ec=error_code(static_cast<int>(e),&generic_category()) |
對于表示簡單錯誤代碼的類型,error_code 提供了許多成員。它基本上是一個從整數到指向 error_category 的指針的簡單映射:
class error_code {
public:
// representation: {value,categor y} of type {int,const error_category*}
};
error_category 是指向其派生類對象的接口。因此,error_category 通過引用傳遞,并以指針形式存儲。每個單獨的 error_category 都由一個唯一的對象表示。
再次考慮 open_file() 這個例子:
ostream& open_file(const string& path)
{
auto dn = split_into_directory_and_name(path); // split into {path,name}
if (error_code err {does_directory_exist(dn.first)}) { // ask "the system" about the path
if (err==errc::permission_denied) {
// ...
}
else if (err==errc::not_a_director y) {
// ...
}
throw system_error(err); // can’t do anything locally
}
// ...
return ofstream{path};
}
errc 錯誤代碼在 §30.4.3.6 中描述。請注意,我使用了 if-then-else 語句鏈,而不是更明顯的 switch 語句。原因是 == 是根據等價關系定義的,同時考慮了錯誤類別 () 和錯誤值 ()。
對 error_code 的操作是系統特定的。在某些情況下,可以使用 §30.4.3.5 中描述的機制將 error_code 映射到 error_conditions (§30.4.3.4)。使用 default_error_condition() 從 error_code 中提取 error_condition。error_condition 通常包含的信息比 error_code 少,因此通常最好保留 error_code,并僅在需要時提取其 error_condition。
??? 操作 error_codes 不會改變 errno 的值(§13.1.2,§40.3)。標準庫保留其他庫提供的錯誤狀態不變。
30.4.3.2? ?錯誤分類
??? error_category 表示錯誤的分類。具體錯誤由從 error_category 類派生的類表示:
class error_categor y {
public:
// ... 從 error_category 派生的特定類別的接口 ...
};
error_category(§iso.19.5.1.1) | |
cat.?error_categor y() | 析構函數;聲明為 virtual, noexcept 。 |
s=cat.name() | s是cat的名稱;s是C風格字符串;聲明為 virtual, noexcept 。 |
ec=cat.default_error_condition(n) | 對于cat中的n,ec是一個error_condition,聲明為 virtual, noexcept 。 |
cat.equivalent(n,ec) | ec.category()==cat 且 ec.value()==n 嗎?ec是一個error_condition,聲明為 virtual, noexcept 。 |
cat.equivalent(ec,n) | ec.category()==cat 且 ec.value()==n 嗎?ec是一個error_code,聲明為 virtual, noexcept 。 |
s=cat.message(n) | s是一個cat中描述n的string 。聲明為 virtual 。 |
cat==cat2 | cat與cat2的分類相同嗎?聲明為noexcept 。 |
cat!=cat2 | !(cat==cat2); 聲明為noexcept 。 |
cat<cat2 | cat<cat2 是否按照基于錯誤類別的順序排列?地址:std::less<const error_categor y?>()(cat, cat2)? 聲明為noexcept 。 |
由于 error_category 被設計為基類,因此不提供復制或移動操作。通過指針或引用訪問 error_category。
有四個命名的標準庫類別:
標準庫錯誤類別 | |
ec=generic_category() | ec.name()=="generic"; ec是一個指向 error_category ?的引用。 |
ec=system_category() | ec.name()=="system" ;ec是一個指向 error_category ?的引用;表示系統錯誤:如果 ec對應于 POSIX 錯誤,則 ec.value() 等于該錯誤的 errno 。 |
ec=future_category() | ec.name()=="future"; ec是一個指向 error_category ?的引用。 |
ostream_category() | ec.name()=="iostream"; ec是一個指向 error_category ?的引用;表示自庫 iostream 的錯誤。 |
這些類別是必要的,因為一個簡單的整數錯誤代碼在不同的上下文中可能具有不同的含義(categorys)。例如,1 在 POSIX 中表示“操作不允許”(EPERM),對于 iostream 錯誤來說,它是所有錯誤的通用代碼(state),而對于 future 錯誤來說,它表示“未來已檢索”(future_already_retrieved)。
30.4.3.3? ?異常 system_error
??? system_error 用于報告最終源自標準庫中與操作系統相關的部分的錯誤。它會傳遞一個 error_code 和一個可選的錯誤消息字符串:
class system_error : public runtime_error {
public:
// ...
};
system_error (§iso.19.5.6) | |
system_error se {ec,s}; | se 存儲 {ec,s};ec 是錯誤代碼;s 是字符串或 C 風格字符串,作為錯誤消息的一部分 。 |
system_error se {ec}; | se 存儲 {ec};ec 是一個error_code 。 |
system_error se {n,cat,s}; | se 存儲 {error_code{n,cat},s};cat 是一個 error_category 變量,n 是表示 cat 中錯誤的 int 值;s 是字符串或 C 風格字符串,用作錯誤消息的一部分。 |
system_error se {n,cat}; | ec.name()=="iostream"; ec是一個指向 error_category ?的引用;表示自庫 iostream 的錯誤。 |
ec=se.code() | ec 是對 se 的 error_code 的引用;聲明為noexcept 。 |
p=se.what() | p 是 se 錯誤字符串的 C 風格字符串版本;聲明為noexcept 。 |
捕獲system_error的代碼會返回其對應的error_code。例如:
try {
// something
}
catch (system_error& err) {
cout << "caught system_error " << err.what() <<'\n'; // error message
auto ec = err.code();
cout << "category: " << ec.category().what() <<'\n';
cout << "value: " << ec.value() <<'\n';
cout << "message: " << ec.message() <<'\n';
}
當然,非標準庫代碼也可以使用 system_error。此時會傳遞系統特定的 error_code,而不是可移植的 error_condition (§30.4.3.4)。要從 error_code 獲取 error_condition,請使用 default_error_condition() (§30.4.3.1)。
30.4.3.4? ?異常潛在的可移植錯誤條件(Potentially Portable Error Conditions)
潛在可移植錯誤代碼(error_condition)的表示方式與系統特定的error_code幾乎相同:
class error_condition { // potentially portable (§iso.19.5.3)
public:
// like error_code but
// no output operator (<<) and
// no default_error_condition()
};
一般的思想是,每個系統都有一組特定的(“本機”)代碼,這些代碼被映射到潛在的可移植代碼中,以方便需要在多個平臺上工作的程序(通常是庫)的程序員。
30.4.3.5? ?映射錯誤代碼(Mapping Error Codes)
要創建一個包含一組 error_code 和至少一個 error_condition 的 error_category ,首先要定義一個枚舉,其中包含所需的 error_code 值。例如:
enum class future_errc {
broken_promise = 1,
future_already_retrieved,
promise_already_satisfied,
no_state
};
這些值的含義完全是特定于類別的。這些枚舉器的整數值是由實現定義的。
future的錯誤類別是標準的一部分,因此讀者可以在標準庫中找到它。具體細節可能與我描述的有所不同。
??? 接下來,我們需要為我們的錯誤代碼定義一個合適的類別:
class future_cat : error_category { // 從 future_category() 返回
public:
const char? name() const noexcept override { return "future"; }
string message(int ec) const override;
};
const error_categor y& future_categor y() noexcept
{
static future_cat obj;
return &obj;
}
從整數值到錯誤消息字符串的映射有點繁瑣。我們必須設計一組對程序員來說可能有意義的消息。在這里,我并不是想耍小聰明:
string future_error::message(int ec) const
{
switch (ec) {
default: return "bad future_error code";
future_errc::broken_promise: return "future_error: broken promise";
future_errc::future_already_retrieved: return "future_error: future already retrieved";
future_errc::promise_already_satisfied: return "future_error: promise already satisfied";
future_errc::no_state: return "future_error: no state";
}
}
現在我們可以從future_errc 生成一個 error_code :
error_code make_error_code(future_errc e) noexcept
{
return error_code{int(e),future_categor y()};
}
對于接受單個錯誤值的error_code構造函數和賦值操作,要求參數的類型應與錯誤類別相匹配。例如,一個旨在成為future_category()的error_code的value()必須是future_errc。特別是,我們不能隨便使用任何整數。例如:
error_code ec1 {7}; // error
error_code ec2 {future_errc::no_state}; // OK
ec1 = 9; // error
ec2 = future_errc::promise_already_satisfied; // OK
ec2 = errc::broken_pipe; // error : wrong error category
?????? 為了幫助 error_code 的實現者,我們為我們的枚舉專門設計了特征 is_error_code_enum:
template<>
struct is_error_code_enum<future_errc> : public true_type { };
標準已經提供了通用模板:
template<typename>
struct is_error_code_enum : public false_type { };
這說明任何我們認為不是錯誤代碼的值都不是錯誤代碼。為了使 error_condition 適用于我們的類別,我們必須重復對 error_code 所做的操作。例如:
error_condition make_error_condition(future_errc e) noexcept;
template<>
struct is_error_condition_enum<future_errc> : public true_type { };
為了實現更有趣的設計,我們可以為 error_condition 使用一個單獨的枚舉,并讓 make_error_condition() 實現從 future_errc 到該枚舉的映射。
30.4.3.6? ?errc錯誤代碼
??? system_category() 的標準錯誤代碼由枚舉類 errc 定義,其值等同于 POSIX 派生的 <cerrno> 內容:
這些代碼適用于“系統”類別:system_category()。對于支持類 POSIX 設施的系統,它們也適用于“通用”類別:generic_category()。
??? POSIX 宏是整數,而 errc 枚舉器是 errc 類型。例如:
void problem(errc e)
{
if (e==EPIPE) { // error : 不存在從errc 到 int的轉換
// ...
}
if (e==broken_pipe) { // error : broken_pipe 不在作用域內
// ...
}
if (e==errc::broken_pipe) { // OK
// ...
}
}
30.4.3.7? ?future_errc錯誤代碼
??? future_category() 的標準錯誤代碼由枚舉類 future_errc 定義:
這些代碼對于“future”類別有效:future_category()。
30.4.3.8? ?io_errc錯誤代碼
iostream_category() 的標準錯誤代碼由枚舉類 io_errc 定義:
此代碼對“iostream”類別有效:iostream_category()。
30.5?? 建議
[1] 使用標準庫工具來保持可移植性;§30.1,§30.1.1。
[2] 使用標準庫工具來最小化維護成本;§30.1。
[3] 使用標準庫工具作為更廣泛、更專業的庫的基礎;§30.1.1。
[4] 使用標準庫工具作為靈活、廣泛使用的軟件的模型;§30.1.1。
[5] 標準庫工具在命名空間 std 中定義,并可在標準庫頭文件中找到;§30.2。
[6] C 標準庫頭文件 X.h 在 <cX> 中作為 C++ 標準庫頭文件呈現;§30.2。
[7] 請勿在未 #include 其頭文件的情況下嘗試使用標準庫工具;§30.2。
[8] 要在內置數組上使用范圍for,請 #include<iterator>;§30.3.2。
[9] 優先使用基于異常的錯誤處理,而不是基于返回碼的錯誤處理;§30.4。
[10] 始終捕獲 exception& (用于標準庫和語言支持異常)和 ...(用于意外異常);§30.4.1。
[11] 標準庫異常層次結構可以(但不是必須)用于用戶自己的異常;§30.4.1.1。
[12] 出現嚴重問題時調用 terminate();§30.4.1.3。
[13] 廣泛使用 static_assert() 和 assert();§30.4.2。
[14] 不要假設 assert() 總是被求值;§30.4.2。
[15] 如果不能使用異常,請考慮 <system_error>;§30.4.3。
內容來源:
<<The?C++?Programming?Language >> 第4版,作者 Bjarne Stroustrup