使用 Macro 和 Lambda 簡化 Exception Handling

| | 0 Comments| 11:29
Categories:

本文主要參考 Visual C Team Blog 的《Exception Boundaries: Working With Multiple Error Handling Mechanisms》一文,取其文章後半段《Unexceptional Consumers》而寫的。

基本上,exception handling 是用 try{}catch{} 的方法,來接收程式所丟(throw)出來的 exception,並針對接收到的 exception 來做對應的處理。而在《Exception Boundaries: Working With Multiple Error Handling Mechanisms》這篇文章裡,也有給了一個簡單的 exception handling 的範例程式:

BOOL DiffFiles(const wchar_t* file1, const wchar_t* file2)
{
try
{
File f1(file1);
File f2(file2);
if (!DiffHandles(f1, f2))
{
SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
return FALSE;
}
return TRUE;
}
catch(const Win32Exception& e)
{
SetLastError(e.GetErrorCode());
}
catch(const std::exception& e)
{
SetLastError(MY_APPLICATION_GENERAL_ERROR);
}
return FALSE;
}

在這個範例裡面,黃底的部份的程式,就是實際要執行的程式碼,而這部份的程式碼會被放在 try{} 的這個區塊裡,讓電腦知道裡面的程式碼是要去做 exception handling 的;而之後,則是接著 catch{} 的區塊,來負責接收 try{} 區塊裡所丟出來的 exception,並針對接收到的 exception 來做對應的處理。

不過 Heresy 這邊這的重點不是 exception handling 的基本概念使用,所以也不打算多提,這邊就算是帶過了;有興趣了解更多的人可以參考 cplusplus.com 的《Exceptions》一文,他有做簡單的說明,或是 MSDN 的《C Exception Handling》系列的文章,也有相對完整的說明。

而實際上在寫程式,如果要處理的夠完整的話, try{} catch{} 這種 exception handling 的方法,在所有可能會產生 exception 的區段,都必須要做,而且這方面的程式碼並不見得很容易可以共用,所以實際上要寫的話,會有不少寫程式上的負擔。而如果 catch{} 的區段要做的事又更複雜的話,那要寫的完整,就又更麻煩了∼

在這篇文章裡,作者提供了兩種方法,可以來簡化這種 exception handling 的寫法,讓程式開發者可以把這方面的程式碼也拿來重複使用;其中一個是使用 Macro 的方法來定義,另一個則是用 C 0x 的 lambda 來做了∼

使用 Macro

這個方法相當地單純,就是把前後區段,都用 #define 的方法來把程式碼定義成 macro,如此一來,之後就可以很簡單地重複利用這些定義好的 macro 了∼

其定義方法如下:

#define WIN32_START try {
#define WIN32_END } catch (const Win32Exception& e) { SetLastError(e.GetErrorCode()); } catch (const std::exception& e) { SetLastError(MY_APPLICATION_GENERAL_ERROR); } return FALSE;

而要使用的時候,也就變得很簡單,會變成:

BOOL DiffFiles(const wchar_t* file1, const wchar_t* file2)
{
WIN32_START
File f1(file1);
File f2(file2);
if (!DiffHandles(f1, f2))
{
SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
return FALSE;
}
return TRUE;
WIN32_END
}

也就是,如果 try{} catch{} 的模式是相同、有定義過的話,那就只要在程式碼的前後,加上定義好的 macro WIN32_STARTWIN32_END 就可以了,而不需要每次都去重新寫一個 try{} catch{} 的內容。

 

使用 lambda

這個方法,則是使用 C 0x 所提供的 lambda expression(參考)搭配 function object 的概念(參考)來做。

基本上,這個方法是將使用 try{} catch{} 進行 exception handling 的部分,寫成一個 template function,而真正要執行的程式碼,則是以 function object 的形式傳遞進來執行(紅色粗體的部分)。

template<typename Func>
BOOL Win32ExceptionBoundary(Func&& f)
{
try
{
return f();
}
catch(const Win32Exception& e)
{
SetLastError(e.GetErrorCode());
}
catch(const std::exception& e)
{
SetLastError(MY_APPLICATION_GENERAL_ERROR);
}
return FALSE;
}

在使用時呢,理論上應該是要把要進行 exception handling 的程式碼,另外獨立包成一個函式後在當成物件傳給 Win32ExceptionBoundary() 來執行;不過實際上這樣會產生許多零星的小函式,會穰整個程式變得很瑣碎。

而現在拜 C 0x 所賜,我們可以直接透過 lambda expression,在不需要額外宣告一個函式的情況下,把一個區段的程式碼包成一個臨時的匿名函式,直接丟給 Win32ExceptionBoundary() 來執行了∼下面就是實際上的寫法:

BOOL DiffFiles(const wchar_t* file1, const wchar_t* file2)
{
return Win32ExceptionBoundary(
[&](){
File f1(file1);
File f2(file2);
if (!DiffHandles(f1, f2))
{
SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
return FALSE;
}
return TRUE;
}
);
}

在上面的程式碼裡,黃底的部分就是原來要執行的程式碼,基本上和之前是完全相同的;而外層的紅底,則就是把它用 lambda expression 來做打包的寫法,這樣整個區塊基本上就可以視為是一個 function object 來使用,所以也可以把它當作參數傳遞給 Win32ExceptionBoundary()

如此一來,只要是使用同樣 exception handling 方法的程式碼,都可以像這樣子透過 lambda expression 包裝,傳遞給 Win32ExceptionBoundary() 執行、並進行 exception handling 了。

在 Heresy 來看,這兩種方法都算是可以有效簡化 exception handling 的方法,都可以在不需要大幅增加、修改程式碼的情況下,將現有的程式碼加上 exception handling。

使用 macro 的好處是,他基本上是會在程式進行編譯的前處理階段,就會處理掉的東西,所以理論上對於程式執行上不會有任何效能上的影響;但是相對的,Heresy 會覺得使用 macro 會比較不好結構化、也不好除錯。而使用 lambda expression 和 function object 的方法,則應該比較容易結構化、物件化,某方面來說應該也會比較容易進行管理和偵錯;但是相對的,他需要新一代的編譯器(Visual C 2010、GCC 4.5)才能使用,而且應該會有一定程度的效能影響。所以,要用哪一種方法呢?基本上,Heresy 覺得也是看要怎麼取捨了。


註:

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *