天天看點

C++/C++11中std::runtime_error的使用

std::runtime_error:運作時錯誤異常類,隻有在運作時才能檢測到的錯誤,繼承于std::exception,它的聲明在頭檔案<stdexcept>中。std::runtime_error也用作幾個運作時錯誤異常的基類,包括std::range_error(生成的結果超出了有意義的值域範圍)、overflow_error(上溢)、underflow_error(下溢)、system_error(系統錯誤)。std::runtime_error類沒有預設構造函數,有兩個聲明為explicit的構造函數,一個接收參數為const char*類型,一個接收參數為const std::string&,這些實參負責提供關于錯誤的更多資訊。std::runtime_error類還有一個繼承自std::exception類的what虛函數,傳回關于異常的一些文本資訊。

std::runtime_error is a more specialized class, descending from std::exception, intended to be thrown in case of various runtime errors. It has a dual purpose. It can be thrown by itself, or it can serve as a base class to various even more specialized types of runtime error exceptions, such as std::range_error,std::overflow_error etc. You can define your own exception classes descending from std::runtime_error, as well as you can define your own exception classes descending from std::exception.

以下内容摘自:《C++Primer(Fifth Edition)》

異常是指在程式運作時發生的反常行為,這些行為超出了函數正常功能的範圍。典型的異常包括失去資料庫連接配接以及遇到意外輸入等。當程式的某部分檢測到一個它無法處理的問題時,需要用到異常處理。此時,檢測出問題的部分應該發出某種信号以表明程式遇到了故障,無法繼續下去了,而且信号的發出方無須知道故障将在何處得到解決。一旦發出異常信号,檢測出問題的部分也就完成了任務。

異常提供了一種轉移程式控制權的方式。C++異常處理涉及到三個關鍵字:try、catch、throw。關于這三個關鍵字的簡單使用可以參考:  http://blog.csdn.net/fengbingchun/article/details/65939258

        異常處理機制為程式中異常檢測和異常處理這兩部分的協作提供支援。在C++語言中,異常處理包括:

        (1)、throw表達式(throw expression):異常檢測部分使用throw表達式來表示它遇到了無法處理的問題。throw引發(raise)異常。throw表達式包含關鍵字throw和緊随其後的一個表達式,其中表達式的類型就是抛出的異常類型。throw表達式後面通常緊跟一個分号,進而構成一條表達式語句。抛出異常将終止目前的函數,并把控制權轉移給能處理該異常的代碼。

        (2)、try語句塊(try block):異常處理部分使用try語句塊處理異常。try語句塊以關鍵字try開始,并以一個或多個catch子句(catch clause)結束。try語句塊中代碼抛出的異常通常會被某個catch子句處理。因為catch子句處理異常,是以它們也被稱作異常處理代碼(exception handler)。catch子句包括三部分:關鍵字catch、括号内一個(可能未命名的)對象的聲明(稱作異常聲明,exception declaration)以及一個塊。當選中了某個catch子句處理異常之後,執行與之對應的塊。catch一旦完成,程式跳轉到try語句塊最後一個catch子句之後的那條語句繼續執行。一如往常,try語句塊聲明的變量在塊外部無法通路,特别是在catch子句内也無法通路。如果一段程式沒有try語句塊且發生了異常,系統會調用terminate函數并終止目前程式的執行。

        (3)、一套異常類(exception class):用于在throw表達式和相關的catch子句之間傳遞異常的具體資訊。

        函數在尋找處理代碼的過程中退出:尋找處理代碼的過程與函數調用鍊剛好相反。當異常被抛出時,首先搜尋抛出該異常的函數。如果沒有找到比對的catch子句,終止該函數,并在調用該函數的函數中繼續尋找。如果還是沒有找到比對的catch子句,這個新的函數也被終止,繼續搜尋調用它的函數。以此類推,沿着程式的執行路徑逐層回退,直到找到适當類型的catch子句為止。如果最終還是沒能找到任何比對的catch子句,程式轉到名為terminate的标準庫函數。該函數的行為與系統有關,一般情況下,執行該函數将導緻程式非正常退出。

        如果一段程式沒有try語句塊且發生了異常,系統會調用terminate函數并終止目前程式的執行。

        那些在異常發生期間正确執行了”清理”工作的程式被稱作異常安全(exception safe)的代碼。編寫異常安全的代碼非常困難。

标準異常:C++标準庫定義了一組類,用于報告标準庫函數遇到的問題。這些異常類也可以在使用者編寫的程式中使用,它們分别定義在4個頭檔案中:

(1)、exception頭檔案定義了最通常的異常類exception,它隻報告異常的發生,不提供任何額外的資訊。

(2)、stdexcept頭檔案定義了幾種常用的異常類,如下:

C++/C++11中std::runtime_error的使用

(3)、new頭檔案定義了bad_alloc異常類型。

(4)、type_info頭檔案定義了bad_cast異常類型。

标準庫異常類隻定義了幾種運算,包括建立或拷貝異常類型的對象,以及為異常類型的對象指派。我們隻能以預設初始化的方式初始化exception、bad_alloc和bad_cast對象,不允許為這些對象提供初始值。其它異常類型的行為則恰恰相反:應該使用string對象或者C風格字元串初始化這些類型的對象,但是不允許使用預設初始化的方式。當建立此類對象時,必須提供初始值,該初始值含有錯誤相關的資訊。

異常類型隻定義了一個名為what的成員函數,該函數沒有任何參數,傳回值是一個指向C風格字元串的const char*。該字元串的目的是提供關于異常的一些文本資訊。what函數傳回的C風格字元串的内容與異常對象的類型有關。如果異常類型有一個字元串初始值,則what傳回該字元串。對于其它無初始值的異常類型來說,what傳回的内容由編譯器決定。

異常處理(exception handling)機制允許程式中獨立開發的部分能夠在運作時就出現的問題進行通信并做出相應的處理。異常使得我們能夠将問題的檢測與解決過程分離開來。程式的一部分負責檢測問題的出現,然後解決該問題的任務傳遞給程式中的另一部分。檢測環節無須知道問題處理子產品的所有細節,反之亦然。

        抛出異常:在C++語言中,我們通過抛出(throwing)一條表達式來引發(raised)一個異常。被抛出的表達式的類型以及目前的調用鍊共同決定了哪段處理代碼(handler)将被用來處理該異常。被選中的處理代碼是在調用鍊中與抛出對象類型比對的最近的處理的代碼。其中,根據抛出對象的類型和内容,程式的異常抛出部分将會告知異常處理部分到底發生了什麼錯誤。

        當執行一個throw時,跟在throw後面的語句将不再被執行,throw語句的用法有點類似于return語句:它通常作為調節語句的一部分或者作為某個函數的最後(或者唯一)一條語句。相反,程式的控制權從throw轉移到與之比對的catch子產品。該catch可能是同一個函數中的局部catch,也可能位于直接或間接調用了發生異常的函數的另一個函數中。

        當抛出一個異常後,程式暫停目前函數的執行過程并立即開始尋找與異常比對的catch子句。當throw出現在一個try語句塊内時,檢查與該try塊關聯的catch子句。如果找到了比對的catch,就使用該catch處理異常。如果這一步沒找到比對的catch且該try語句嵌套在其它try塊中,則繼續檢查與外層try比對的catch子句。如果還是找不到比對的catch,則退出目前的函數,在調用目前函數的外層函數中繼續尋找,依次類推。這一過程被稱為棧展開(stack unwinding)過程。棧展開過程沿着嵌套函數的調用鍊不斷查找,直到找到了與異常比對的catch子句為止:或者也可能一直沒找到比對的catch,則退出主函數後查找過程終止。

        假設找到了一個比對的catch子句,則程式進入該子句并執行其中的代碼。當執行完這個catch子句後,找到與try塊關聯的最後一個catch子句之後的點,并從這裡繼續執行。如果沒找到比對的catch子句,程式将退出。因為異常通常被認為是妨礙程式正常執行的事件,是以一旦引發了某個異常,就不能對它置之不理。當找不到比對的catch時,程式将調用标準庫函數terminate,terminate負責終止程式的執行過程。

        Note:一個異常如果沒有被捕獲,則它将終止目前的程式。

        棧展開過程中對象被自動銷毀:在棧展開過程中,位于調用鍊上的語句塊可能會提前退出。通常情況下,程式在這些塊中建立了一些局部對象。塊退出後它的局部對象也将随之銷毀,這條規則對于棧展開過程同樣适用。如果在棧展開過程中退出了某個塊,編譯器将負責確定在這個塊中建立的對象能被正确地銷毀。如果某個局部對象的類型是類類型,則該對象的析構函數将被自動調用。與往常一樣,編譯器在銷毀内置類型的對象時不需要做任何事情。

        析構函數與異常:析構函數總是會被執行的。出于棧展開可能使用析構函數的考慮,析構函數不應該抛出不能被它自身處理的異常。換句話說,如果析構函數需要執行某個可能抛出異常的操作,則該操作應該被放置在一個try語句塊當中,并且在析構函數内部得到處理。一旦在棧展開的過程中析構函數抛出了異常,并且析構函數自身沒能捕獲到異常,則程式将被終止。

        異常對象(exception object):是一種特殊的對象,編譯器使用異常抛出表達式來對異常對象進行拷貝初始化。是以,throw語句中的表達式必須擁有完全類型。而且如果該表達式是類類型的話,則相應的類必須含有一個可通路的析構函數和一個可通路的拷貝或移動構造函數。如果該表達式是數組類型或函數類型,則表達式将被轉換成與之對應的指針類型。異常對象位于由編譯器管理的空間中,編譯器確定無論最終調用的是哪個catch子句都能通路該空間。當異常處理完畢後,異常對象被銷毀。如果退出了某個塊,則同時釋放塊中局部對象使用的記憶體。是以,抛出一個指向局部對象的指針幾乎肯定是一種錯誤的行為。出于同樣的原因,從函數中傳回指向局部對象的指針也是錯誤的。當我們抛出一條表達式時,該表達式的靜态編譯時類型決定了異常對象的類型。如果一條throw表達式解引用一個基類指針,而該指針實際指向的是派生類對象,則抛出的對象将被切掉一部分,隻有基類部分被抛出。

        捕獲異常:catch子句(catch clause)中的異常聲明(exception declaration)看起來像是隻包含一個形參的函數形參清單。像在形參清單中一樣,如果catch無須通路抛出的表達式的話,則我們可以忽略捕獲形參的名字。聲明的類型決定了處理代碼所能捕獲的異常類型.這個類型必須是完全類型,它可以是左值引用,但不能是右值引用。

        當進入一個catch語句後,通過異常對象初始化異常聲明中的參數。和函數的參數類似,如果catch的參數類型是非引用類型,則該參數是異常對象的一個副本,在catch語句内改變參數實際上改變的是局部副本而非異常對象本身;相反,如果參數是引用類型,則和其它引用參數一樣,該參數是異常對象的一個别名,此時改變參數也就是改變異常對象。

        catch的參數還有一個特性也與函數的參數非常類似:如果catch的參數是基類類型,則我們可以使用其派生類類型的異常對象對其進行初始化。此時,如果catch的參數是非引用類型,則異常對象将被切掉一部分,這與将派生類對象以值傳遞的方式傳給一個普通函數差不多。另一方面,如果catch的參數是基類的引用,則該參數将以正常方式綁定到異常對象上。

        異常聲明的靜态類型将決定catch語句所能執行的操作。如果catch的參數是基類類型,則catch無法使用派生類特有的任何成員。

        通常情況下,如果catch接受的異常與某個繼承體系有關,則最好将該catch的參數定義成引用類型。

        查找比對的處理代碼:在搜尋catch語句的過程中,我們最終找到的catch未必是異常的最佳比對。相反,挑選出來的應該是第一個與異常比對的catch語句。是以,越是專門的catch越應該置于整個catch清單的前端。因為catch語句是按照其出現的順序逐一進行比對的,是以當程式使用具有繼承關系的多個異常時必須對catch語句的順序進行組織和管理,使得派生類異常的處理代碼出現在基類異常的處理代碼之前。

        與實參和形參的比對規則相比,異常和catch異常聲明的比對規則受到更多限制。此時,絕大多數類型轉換都不被允許,除了一些極細小的差别之外,要求異常的類型和catch聲明的類型是精确比對的:

(1)、允許在非常量向常量的類型轉換,也就是說,一條非常量對象的throw語句可以比對一個接受常量引用的catch語句。

(2)、允許從派生類向基類的類型轉換。

(3)、數組被轉換成指向數組(元素)類型的指針,函數被轉換成指向該函數類型的指針。

除此之外,包括标準算術類型轉換和類類型轉換在内,其它所有轉換規則都不能在比對catch的過程中使用。

如果在多個catch語句的類型之間存在着繼承關系,則我們應該把繼承鍊最低端的類(most derived type)放在前面,而将繼承鍊最頂端的類(least derived type)放在後面。

重新抛出:有時,一個單獨的catch語句不能完整地處理某個異常。在執行了某些校正操作之後,目前的catch可能會決定由調用鍊更上一層的函數接着處理異常。一條catch語句通過重新抛出(rethrowing)的操作将異常傳遞給另外一個catch語句。這裡的重新抛出仍然是一條throw語句,隻不過不包含任何表達式:throw;

空的throw語句隻能出現在catch語句或catch語句直接或間接調用的函數之内。如果在處理代碼之外的區域遇到了空throw語句,編譯器将調用terminate。

一個重新抛出語句并不指定新的表達式,而是将目前的異常對象沿着調用鍊向上傳遞。

很多時候,catch語句會改變其參數的内容。如果在改變了參數的内容後catch語句重新抛出異常,則隻有當catch異常聲明是引用類型時我們對參數所做的改變才會被保留并繼續傳播。

捕獲所有異常的處理代碼:為了一次性捕獲所有異常,我們使用省略号作為異常聲明,這樣的處理代碼稱為捕獲所有異常(catch-all)的處理代碼,形如catch(…)。一條捕獲所有異常的語句可以與任意類型的異常比對。

catch(…)通常與重新抛出語句一起使用,其中catch執行目前局部能完成的工作,随後重新抛出異常。

catch(…)既能單獨出現,也能與其它幾個catch語句一起出現。

如果catch(…)與其它幾個catch語句一起出現,則catch(…)必須在最後的位置。出現在捕獲所有異常語句後面的catch語句将永遠不會被比對。

函數try語句塊與構造函數:通常情況下,程式執行的任何時刻都可能發生異常,特别是異常可能發生在處理構造函數初始值的過程中。構造函數在進入其函數體之前首先執行初始值清單。因為在初始值清單抛出異常時構造函數體内的try語句塊還未生效,是以構造函數體内的catch語句無法處理構造函數初始值清單抛出的異常。要想處理構造函數初始值抛出的異常,我們必須将構造函數寫出函數try語句塊(也稱為函數測試塊,function try block)的形式。函數try語句塊使得一組catch語句既能處理構造函數體(或析構函數體),也能處理構造函數的初始化過程(或析構函數的析構過程)。

在初始化構造函數的參數時也可能發生異常,這樣的異常不屬于函數try語句塊的一部分。函數try語句塊隻能處理構造函數開始執行後發生的異常。和其它函數調用一樣,如果在參數初始化的過程中發生了異常,則該異常屬于調用表達式的一部分,并将在調用者所在的上下文中處理。

處理構造函數初始值異常的唯一方法是将構造函數寫成函數try語句塊。

noexcept異常說明:在C++11新标準中,我們可以通過提供noexcept說明(noexcept specification)指定某個函數不會抛出異常。其形式是關鍵字noexcept緊跟在函數的參數清單後面,用以辨別該函數不會抛出異常。

對于一個函數來說,noexcept說明要麼出現在該函數的所有聲明語句和定義語句中,要麼一次也不出現。該說明應該在函數的尾置傳回類型之前。我們也可以在函數指針的聲明和定義中指定noexcept。在typedef或類型别名中則不能出現noexcept。在成員函數中,noexcept說明符需要跟在const及引用限定符之後,而在final、override或虛函數的=0之前。

違反異常說明:如果一個函數在說明了noexcept的同時又含有throw語句或者調用了可能抛出異常的其它函數,編譯器将順利編譯通過,并不會因為這種違反異常說明的情況而報錯(不排除個别編譯器會對這種用法提出警告)。一旦一個noexcept函數抛出了異常,程式就會調用terminate以確定遵守不在運作時抛出異常的承諾。noexcept可以用在兩種情況下,一是我們确認函數不會抛出異常;二是我們根本不知道該如何處理異常。

通常情況下,編譯器不能也不必在編譯時驗證異常說明。

如果函數被設計為是throw()的,則意味着該函數将不會抛出異常:void f(int) throw();

異常說明的實參:noexcept說明符接受一個可選的實參,該實參必須能轉換為bool類型:如果實參是true,則函數不會抛出異常;如果實參是false,則函數可能抛出異常。

noexcept運算符:noexcept說明符的實參常常與noexcept運算符(noexcept orerator)混合使用。noexcept運算符是一個一進制運算符,它的傳回值是一個bool類型的右值常量表達式,用于表示給定的表達式是否會抛出異常。和sizeof類似,noexcept也不會求其運算對象的值。

noexcept有兩層含義:當跟在函數參數清單後面時它是異常說明符;而當作為noexcept異常說明的bool實參出現時,它是一個運算符。

異常說明與指針、虛函數和拷貝控制:盡管noexcept說明符不屬于函數類型的一部分,但是函數的異常說明仍然會影響函數的使用。函數指針及該指針所指的函數必須具有一緻的異常說明。也就是說,如果我們為某個指針做了不抛出異常的說明,則該指針将隻能指向不抛出異常的函數。相反,如果我們顯示或隐式地說明了指針可能抛出異常,則該指針可以指向任何函數,即使是承諾了不抛出異常的函數也可以。

如果一個虛函數承諾了它不會抛出異常,則後續派生出來的虛函數也必須做出同樣的承諾;與之相反,如果基類的虛函數允許抛出異常,則派生類的對應函數既可以允許抛出異常,也可以不允許抛出異常。

當編譯器合成拷貝控制成員時,同時也生成一個異常說明。如果對所有成員和基類的所有操作都承諾了不會抛出異常,則合成的成員是noexcept的。如果合成成員調用的任意一個函數可能抛出異常,則合成的成員是noexcept(false)。而且,如果我們定義了一個析構函數但是沒有為它提供異常說明,則編譯器将合成一個。合成的異常說明将與假設由編譯器為類合成析構函數時所得的異常說明一緻。

異常類層次:标準庫異常類構成了下圖所示的繼承體系:

C++/C++11中std::runtime_error的使用

類型exception僅僅定義了拷貝構造函數、拷貝指派運算符、一個虛析構函數和一個名為what的虛成員。其中what函數傳回一個const char*,該指針指向一個以null結尾的字元數組,并且確定不會抛出任何異常。

類exception、bad_cast和bad_alloc定義了預設構造函數。類runtime_error和logic_error沒有預設構造函數,但是有一個可以接受C風格字元串或者标準庫string類型實參的構造函數,這些實參負責提供關于錯誤的更多資訊。在這些類中,what負責傳回用于初始化異常對象的資訊。因為what是虛函數,是以當我們捕獲基類的引用時,對what函數的調用将執行與異常對象動态類型對應的版本。

實際的應用程式通常會自定義exception(或者exception的标準庫派生類)的派生類以擴充其繼承體系。這些面向應用的異常類表示了與應用相關的異常條件。和其它繼承體系一樣,異常類也可以看作按照層次關系組織的。層次越低,表示的異常情況就越特殊。例如,在異常類繼承體系中位于最頂層的通常是exception,exception表示的含義是某處出錯了,至于錯誤的細節則未作描述。

繼承體系的第二層将exception劃分為兩個大的類别:運作時錯誤和邏輯錯誤。運作時錯誤表示的是隻有在程式運作時才能檢測到的錯誤;而邏輯錯誤一般指的是我們可以在程式代碼中發現的錯誤。

下面是從其他文章中copy的std::exception測試代碼,詳細内容介紹可以參考對應的reference:

#include "runtime_error.hpp"
#include <iostream>
#include <stdexcept>
#include <string>

namespace runtime_error_ {
//
// reference: https://msdn.microsoft.com/en-us/library/tyahh3a9.aspx
int test_runtime_error_1()
{
	try {
		std::locale loc("test");
	} catch (std::exception& e) {
		std::cerr << "Caught " << e.what() << std::endl; // Caught bad locale name
		std::cerr << "Type " << typeid(e).name() << std::endl; // Type class std::runtime_error
	};

	return 0;
}

/
// reference: http://www.java2s.com/Tutorial/Cpp/0120__Exceptions/Throwyourownexceptionclassbasedonruntimeerror.htm
class DivideByZeroException : public std::runtime_error {
public:
	DivideByZeroException::DivideByZeroException() : runtime_error("attempted to divide by zero") {}
};

static double quotient(int numerator, int denominator)
{
	throw DivideByZeroException(); // terminate function
	return 0;
}

int test_runtime_error_2()
{
	try {
		double result = quotient(1, 1);
		std::cout << "The quotient is: " << result << std::endl;
	} catch (DivideByZeroException& divideByZeroException) {
		std::cout << "Exception occurred: " << divideByZeroException.what() << std::endl; // Exception occurred: attempted to divide by zero
	}

	return 0;
}


class CppBase_RunTime_Exception : public std::runtime_error {
public :
	CppBase_RunTime_Exception(int error_code_) : runtime_error(""), error_code(error_code_) {}
	CppBase_RunTime_Exception(int error_code_, const std::string& info_) : runtime_error(info_), error_code(error_code_) {}
	int get_error_code() const { return error_code; }
private:
	int error_code = 0;
};

static int calc(int a)
{
	if (a > 0) {
		throw CppBase_RunTime_Exception(1, __FUNCTION__);
	}

	if (a < 0) {
		throw CppBase_RunTime_Exception(-1, __FUNCTION__);
	}

	throw CppBase_RunTime_Exception(0);

	return 0;
}

int test_runtime_error_3()
{
	const int a{ 2 }, b{ -3 }, c{ 0 };

	try {
		calc(a);
	} catch (const CppBase_RunTime_Exception& e) {
		std::cerr << "error fun name: " << e.what() << ", error code: " << e.get_error_code() << std::endl;
	}

	try {
		calc(b);
	} catch (const CppBase_RunTime_Exception& e) {
		std::cerr << "error fun name: " << e.what() << ", error code: " << e.get_error_code() << std::endl;
	}

	try {
		calc(c);
	} catch (const CppBase_RunTime_Exception& e) {
		std::cerr << "error fun name: " << e.what() << ", error code: " << e.get_error_code() << std::endl;
	}

	std::cout << "over" << std::endl;

	return 0;
}

} // namespace runtime_error_
           

GitHub:  https://github.com/fengbingchun/Messy_Test