# 第?15?章?錯誤處理
### 目錄
* [15.1 概述](errorhandling.html#errorhandling_general)
* [15.2 Boost.System](errorhandling.html#errorhandling_system)
* [15.3 Boost.Exception](errorhandling.html#errorhandling_exception)
[](http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.zh) 該書采用 [Creative Commons License](http://creativecommons.org/licenses/by-nc-nd/3.0/de/deed.zh) 授權
## 15.1.?概述
在執行時會有潛在失敗可能的每個函數都需要一種合適的方式和它的調用者進行交互。 在C++中,這一步是通過返回值或拋出一個異常來完成的。 作為常識,返回值經常用在處理非錯誤的異常中。 調用者通過返回值作出相應的反饋。
異常被通常用來標示出未預期的異常情況。 一個很好的例子是在錯誤的使用 `new` 時將拋出的一個動態內存分配異常類型 `std::bad_alloc` 。 由于內存的分配通常不會出現任何問題,如果總是檢查返回值將會變得異常累贅。
本章介紹了兩種可以幫助開發者利用錯誤處理的Boost C++庫:其中 Boost.System 可以由特定操作系統平臺的錯誤代碼轉換出跨平臺的錯誤代碼。 借助于 Boost.System,函數基于某個特定操作系統的返回值類型可以被轉換成為跨平臺的類型。 另外,Boost.Exception 允許給任何異常添加額外的信息,以便利用 `catch` 相應的處理程序更好的對異常作出反應。
## 15.2.?Boost.System
[Boost.System](http://www.boost.org/libs/system/) 是一個定義了四個類的小型庫,用以識別錯誤。 `boost::system::error_code` 是一個最基本的類,用于代表某個特定操作系統的異常。 由于操作系統通常枚舉異常,`boost::system::error_code` 中以變量的形式保存錯誤代碼 `int`。 下面的例子說明了如何通過訪問 Boost.Asio 類來使用這個類。
```
#include <boost/system/error_code.hpp>
#include <boost/asio.hpp>
#include <iostream>
#include <string>
int main()
{
boost::system::error_code ec;
std::string hostname = boost::asio::ip::host_name(ec);
std::cout << ec.value() << std::endl;
}
```
* [下載源代碼](src/15.2.1/main.cpp)
Boost.Asio 提供了獨立的函數 `boost::asio::ip::host_name()` 可以返回正在執行的應用程序名。
`boost::system::error_code` 類型的一個對象可以作為單獨的參數傳遞給 `boost::asio::ip::host_name()`。 如果當前的操作系統函數失敗, 這個參數包含相關的錯誤代碼。 也可以通過調用 `boost::asio::ip::host_name()` 而不使用任何參數,以防止錯誤代碼是非相關的。
事實上在Boost 1.36.0中 `boost::asio::ip::host_name()` 是有問題的,然而它可以當作一個很好的例子。 即使當前操作系統函數成功返回了計算機名,這個函數它也可能返回一個錯誤代碼。 由于在Boost 1.37.0中解決了這個問題,現在可以放心使用 `boost::asio::ip::host_name()` 了。
由于錯誤代碼僅僅是一個數值,因此可以借助于 `value()` 方法得到它。 由于錯誤代碼0通常意味著沒有錯誤,其他的值的意義則依賴于操作系統并且需要查看相關手冊。
如果使用Boost 1.36.0, 并且用Visual Studio 2008在Windows XP環境下編譯以上應用程序將不斷產生錯誤代碼14(沒有足夠的存儲空間以完成操作)。 即使函數 `boost::asio::ip::host_name()` 成功決定了計算機名,也會報出錯誤代碼14。 事實上這是因為函數 `boost::asio::ip::host_name()` 的實現有問題。
除了 `value()` 方法之外, 類 `boost::system::error_code` 提供了方法 `category()`。 這個方法可返回一個在 Boost.System 中定義的二級對象: `boost::system::category`。
錯誤代碼是簡單的數值。 操作系統開發商,例如微軟,可以保證系統錯誤代碼的特異性。 對于任何開發商來說,在所有現有應用程序中保持錯誤代碼的獨一無二是幾乎不可能的。 他需要一個包含有所有軟件開發者的錯誤代碼中心數據庫,以防止在不同的方案下重復使用相同的代碼。 當然這是不實際的。 這是錯誤分類表存在的緣由。
類型 `boost::system::error_code` 的錯誤代碼總是屬于可以使用 `category()` 方法獲取的分類。 通過預定義的對象 `boost::system::system_category` 來表示操作系統的錯誤。
通過調用 `category()` 方法,可以返回預定義變量 `boost::system::system_category` 的一個引用。 它允許獲取關于分類的特定信息。 例如在使用的是 system 分類的情況下,通過使用 `name()` 方法將得到它的名字 `system`。
```
#include <boost/system/error_code.hpp>
#include <boost/asio.hpp>
#include <iostream>
#include <string>
int main()
{
boost::system::error_code ec;
std::string hostname = boost::asio::ip::host_name(ec);
std::cout << ec.value() << std::endl;
std::cout << ec.category().name() << std::endl;
}
```
* [下載源代碼](src/15.2.2/main.cpp)
通過錯誤代碼和錯誤分類識別出的錯誤是獨一無二的。 由于僅僅在錯誤分類中的錯誤代碼是必須唯一的,程序員應當在希望定義某個特定應用程序的錯誤代碼時創建一個新的分類。 這使得任何錯誤代碼都不會影響到其他開發者的錯誤代碼。
```
#include <boost/system/error_code.hpp>
#include <iostream>
#include <string>
class application_category :
public boost::system::error_category
{
public:
const char *name() const { return "application"; }
std::string message(int ev) const { return "error message"; }
};
application_category cat;
int main()
{
boost::system::error_code ec(14, cat);
std::cout << ec.value() << std::endl;
std::cout << ec.category().name() << std::endl;
}
```
* [下載源代碼](src/15.2.3/main.cpp)
通過創建一個派生于 `boost::system::error_category` 的類以及實現作為新分類的所必須的接口的不同方法可以定義一個新的錯誤分類。 由于方法 `name()` 和 `message()` 在類 `boost::system::error_category` 中被定義為純虛擬函數,所以它們是必須提供的。 至于額外的方法,在必要的條件下,可以重載相對應的默認行為。
當方法 `name()` 返回錯誤分類名時,可以使用方法 `message()` 來獲取針對某個錯誤代碼的描述。 不像之前的那個例子,參數 `ev` 往往被用于返回基于錯誤代碼的描述。
新創建的錯誤分類的對象可以被用來初始化相應的錯誤代碼。 本例中定義了一個用于新分類 `application_category` 的錯誤代碼 `ec` 。 然而錯誤代碼14不再是系統錯誤;他的意義被開發者指定為新的錯誤分類。
`boost::system::error_code` 包含了一個叫作 `default_error_condition()` 的方法,它可以返回 `boost::system::error_condition`類型的對象。 `boost::system::error_condition` 的接口幾乎與 `boost::system::error_code` 相同。 唯一的差別是只有類 `boost::system::error_code` 提供了方法 `default_error_condition()` 。
```
#include <boost/system/error_code.hpp>
#include <boost/asio.hpp>
#include <iostream>
#include <string>
int main()
{
boost::system::error_code ec;
std::string hostname = boost::asio::ip::host_name(ec);
boost::system::error_condition ecnd = ec.default_error_condition();
std::cout << ecnd.value() << std::endl;
std::cout << ecnd.category().name() << std::endl;
}
```
* [下載源代碼](src/15.2.4/main.cpp)
`boost::system::error_condition` 的使用方法與 `boost::system::error_code` 類似。 對象`boost::system::error_condition` 的 `value()` 和 `category()` 方法都可以像上面的例子中那樣調用。
有或多或少兩個相同的類的原因很簡單:當類 `boost::system::error_code` 被當作當前平臺的錯誤代碼時, 類 `boost::system::error_condition` 可以被用作獲取跨平臺的錯誤代碼。 通過調用 `default_error_condition()` 方法,可以把依賴于某個平臺的的錯誤代碼轉換成 `boost::system::error_condition` 類型的跨平臺的錯誤代碼。
如果執行以上應用程序,它將顯示數字12以及錯誤分類 `GENERIC`。 依賴于平臺的錯誤代碼14被轉換成了跨平臺的錯誤代碼12。 借助于 `boost::system::error_condition` ,可以總是使用相同的數字表示錯誤,無視當前操作系統。 當Windows報出錯誤14時,其他操作系統可能會對相同的錯誤報出錯誤代碼25。 使用 `boost::system::error_condition` ,總是對這個錯誤報出錯誤代碼12。
最后 Boost.System 提供了類 `boost::system::system_error` ,它派生于 `std::runtime_error`。 它可被用來傳送發生在異常里類型為 `boost::system::error_code` 的錯誤代碼。
```
#include <boost/asio.hpp>
#include <boost/system/system_error.hpp>
#include <iostream>
int main()
{
try
{
std::cout << boost::asio::ip::host_name() << std::endl;
}
catch (boost::system::system_error &e)
{
boost::system::error_code ec = e.code();
std::cerr << ec.value() << std::endl;
std::cerr << ec.category().name() << std::endl;
}
}
```
* [下載源代碼](src/15.2.5/main.cpp)
獨立的函數 `boost::asio::ip::host_name()` 是以兩種方式提供的:一種是需要類型為 `boost::system::error_code` 的參數,另一種不需要參數。 第二個版本將在錯誤發生時拋出 `boost::system::system_error` 類型的異常。 異常傳出類型為 `boost::system::error_code` 的相應錯誤代碼。
## 15.3.?Boost.Exception
[Boost.Exception](http://www.boost.org/libs/exception/) 庫提供了一個新的異常類 `boost::exception` 允許給一個拋出的異常添加信息。 它被定義在文件 `boost/exception/exception.hpp` 中。 由于 Boost.Exception 中的類和函數分布在不同的頭文件中, 下面的例子中將使用 `boost/exception/all.hpp` 以避免一個一個添加頭文件。
```
#include <boost/exception/all.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/shared_array.hpp>
#include <exception>
#include <string>
#include <iostream>
typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info;
class allocation_failed :
public boost::exception,
public std::exception
{
public:
allocation_failed(std::size_t size)
: what_("allocation of " + boost::lexical_cast<std::string>(size) + " bytes failed")
{
}
virtual const char *what() const throw()
{
return what_.c_str();
}
private:
std::string what_;
};
boost::shared_array<char> allocate(std::size_t size)
{
if (size > 1000)
throw allocation_failed(size);
return boost::shared_array<char>(new char[size]);
}
void save_configuration_data()
{
try
{
boost::shared_array<char> a = allocate(2000);
// saving configuration data ...
}
catch (boost::exception &e)
{
e << errmsg_info("saving configuration data failed");
throw;
}
}
int main()
{
try
{
save_configuration_data();
}
catch (boost::exception &e)
{
std::cerr << boost::diagnostic_information(e);
}
}
```
* [下載源代碼](src/15.3.1/main.cpp)
這個例子在 `main()` 中調用了一個函數 `save_configuration_data()` ,它調回了 `allocate()` 。 `allocate()` 函數動態分配內存,而它檢查是否超過某個限度。 這個限度在本例中被設定為1,000個字節。
如果 `allocate()` 被調用的值大于1,000,將會拋出 `save_configuration_data()` 函數里的相應異常。 正如注釋中所標識的那樣,這個函數把配置數據被存儲在動態分配的內存中。
事實上,這個例子的目的是通過拋出異常以示范 Boost.Exception。 這個通過 `allocate()` 拋出的異常是 `allocation_failed` 類型的,而且它同時繼承了 `boost::exception` 和 `std::exception`。
當然,也不是一定要派生于 `std::exception` 異常的。 為了把它嵌入到現有的框架中,異常 `allocation_failed` 可以派生于其他類的層次結構。 當通過C++標準來定義以上例子的類層次結構的時候, 單獨從 `boost::exception` 中派生出 `allocation_failed` 就足夠了。
當拋出 `allocation_failed` 類型的異常的時候,分配內存的大小是存儲在異常中的,以緩解相應應用程序的調試。 如果想通過 `allocate()` 分配獲取更多的內存空間,那么可以很容易發現導致異常的根本原因。
如果僅僅通過一個函數(例子中的函數 `save_configuration_data()`)來調用 `allocate()` ,這個信息足以找到問題的所在。 然而,在有許多函數調用 `allocate()` 以動態分配內存的更加復雜的應用程序中,這個信息不足以高效的調試應用程序。 在這些情況下,它最好能有助于找到哪個函數試圖分配 `allocate()` 所能提供空間之外的內存。 向異常中添加更多的信息,在這些情況下,將非常有助于進程的調試。
有挑戰性的是,函數 `allocate()` 中并沒有調用者名等信息,以把它加入到相關的異常中。
Boost.Exception 提供了如下的解決方案:對于任何一個可以添加到異常中的信息,可以通過定義一個派生于 `boost::error_info` 的數據類型,來隨時向這個異常添加信息。
`boost::error_info` 是一個需要兩個參數的模板,第一個參數叫做標簽(tag),特定用來識別新建的數據類型。 通常是一個有特定名字的結構體。 第二個參數是與存儲于異常中的數據類型信息相關的。
這個應用程序定義了一個新的數據類型 `errmsg_info`,可以通過 `tag_errmsg` 結構來特異性的識別,它存儲著一個 `std::string` 類型的字符串。
在 `save_configuration_data()` 的 `catch` 句柄中,通過獲取 `tag_errmsg` 以創建一個對象,它通過字符串 "saving configuration data failed" 進行初始化,以便通過 `operator<<()` 操作符向異常 `boost::exception` 中加入更多信息。 然后這個異常被相應的重新拋出。
現在,這個異常不僅包含有需要動態分配的內存大小,而且對于錯誤的描述被填入到 `save_configuration_data()` 函數中。 在調試時,這個描述顯然很有幫助,因為可以很容易明白哪個函數試圖分配更多的內存。
為了從一個異常中獲取所有可用信息,可以像例子中那樣在 `main()` 的 `catch` 句柄中使用函數 `boost::diagnostic_information()` 。 對于每個異常,函數 `boost::diagnostic_information()` 不僅調用 `what()` 而且獲取所有附加信息存儲到異常中。 返回一個可以在標準輸出中寫入的 `std::string` 字符串。
以上程序通過Visual C++ 2008編譯會顯示如下的信息:
```
Throw in function (unknown)
Dynamic exception type: class allocation_failed
std::exception::what: allocation of 2000 bytes failed
[struct tag_errmsg *] = saving configuration data failed
```
正如我們所看見的,數據包含了異常的數據類型,通過 `what()` 方法獲取到錯誤信息,以及包括相應結構體名的描述。
`boost::diagnostic_information()` 函數在運行時檢查一個給定的異常是否派生于 `std::exception`。 只會在派生于 `std::exception` 的條件下調用 `what()` 方法。
拋出異常類型 `allocation_failed` 的函數名會被指定為"unknown"(未知)信息。
Boost.Exception 提供了一個用以拋出異常的宏,它包含了函數名,以及如文件名、行數的附加信息。
```
#include <boost/exception/all.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/shared_array.hpp>
#include <exception>
#include <string>
#include <iostream>
typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info;
class allocation_failed :
public std::exception
{
public:
allocation_failed(std::size_t size)
: what_("allocation of " + boost::lexical_cast<std::string>(size) + " bytes failed")
{
}
virtual const char *what() const throw()
{
return what_.c_str();
}
private:
std::string what_;
};
boost::shared_array<char> allocate(std::size_t size)
{
if (size > 1000)
BOOST_THROW_EXCEPTION(allocation_failed(size));
return boost::shared_array<char>(new char[size]);
}
void save_configuration_data()
{
try
{
boost::shared_array<char> a = allocate(2000);
// saving configuration data ...
}
catch (boost::exception &e)
{
e << errmsg_info("saving configuration data failed");
throw;
}
}
int main()
{
try
{
save_configuration_data();
}
catch (boost::exception &e)
{
std::cerr << boost::diagnostic_information(e);
}
}
```
* [下載源代碼](src/15.3.2/main.cpp)
通過使用宏 `BOOST_THROW_EXCEPTION` 替代 `throw`, 如函數名、文件名、行數之類的附加信息將自動被添加到異常中。但這僅僅在編譯器支持宏的情況下有效。 當通過C++標準定義 `__FILE__` 和 `__LINE__` 之類的宏時,沒有用于返回當前函數名的標準化的宏。 由于許多編譯器制造商提供這樣的宏, `BOOST_THROW_EXCEPTION` 試圖識別當前編譯器,從而利用相對應的宏。 使用 Visual C++ 2008 編譯時,以上應用程序顯示以下信息:
```
.\main.cpp(31): Throw in function class boost::shared_array<char> __cdecl allocate(unsigned int)
Dynamic exception type: class boost::exception_detail::clone_impl<struct boost::exception_detail::error_info_injector<class allocation_failed> >
std::exception::what: allocation of 2000 bytes failed
[struct tag_errmsg *] = saving configuration data failed
```
即使 `allocation_failed` 類不再派生于 `boost::exception` 代碼的編譯也不會產生錯誤。 `BOOST_THROW_EXCEPTION` 獲取到一個能夠動態識別是否派生于 `boost::exception` 的函數 `boost::enable_error_info()`。 如果不是,他將自動建立一個派生于特定類和 `boost::exception` 的新異常類型。 這個機制使得以上信息中不僅僅顯示內存分配異常 `allocation_failed` 。
最后,這個部分包含了一個例子,它選擇性的獲取了添加到異常中的信息。
```
#include <boost/exception/all.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/shared_array.hpp>
#include <exception>
#include <string>
#include <iostream>
typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info;
class allocation_failed :
public std::exception
{
public:
allocation_failed(std::size_t size)
: what_("allocation of " + boost::lexical_cast<std::string>(size) + " bytes failed")
{
}
virtual const char *what() const throw()
{
return what_.c_str();
}
private:
std::string what_;
};
boost::shared_array<char> allocate(std::size_t size)
{
if (size > 1000)
BOOST_THROW_EXCEPTION(allocation_failed(size));
return boost::shared_array<char>(new char[size]);
}
void save_configuration_data()
{
try
{
boost::shared_array<char> a = allocate(2000);
// saving configuration data ...
}
catch (boost::exception &e)
{
e << errmsg_info("saving configuration data failed");
throw;
}
}
int main()
{
try
{
save_configuration_data();
}
catch (boost::exception &e)
{
std::cerr << *boost::get_error_info<errmsg_info>(e);
}
}
```
* [下載源代碼](src/15.3.3/main.cpp)
這個例子并沒有使用函數 `boost::diagnostic_information()` 而是使用 `boost::get_error_info()` 函數來直接獲取錯誤信息的類型 `errmsg_info`。 函數 `boost::get_error_info()` 用于返回 `boost::shared_ptr` 類型的智能指針。 如果傳遞的參數不是 `boost::exception` 類型的,返回的值將是相應的空指針。 如果 `BOOST_THROW_EXCEPTION` 宏總是被用來拋出異常,派生于 `boost::exception` 的異常是可以得到保障的——在這些情況下沒有必要去檢查返回的智能指針是否為空。