說到Exception就要說下相關的Error Handling. 比較常用的Error Handling一般有如下幾種類方式:
1. Return value
2. Assert
3. Debug Output
4. Exception
相對於其他三種錯誤處理方式, Exception更加容易使用,而且使得錯誤碼相對集中,同時使得獨立函數庫的開發更加方便。同樣,對於C++來說, Exception提供了Class的Constructor 和 Operator = 錯誤處理機制,因為這兩者都不是能夠通過return value進行報錯的。
但是就遊戲開發來說, Exception最大的缺點是記憶體和CPU的開銷。當然,不是說遊戲的代碼中不應該使用Exception。 Aear見過用Exception的遊戲代碼,也有完全不用Exception的代碼。因為對遊戲來說,應該在運行過程中保持自身狀態的正確性,不應該產生任何的無法處理的Exception。而所有能夠自己處理的錯誤情況,都是能夠通過Return value 來解決的。唯一可能產生Exception的地方,就是系統資源,比如磁碟檔案,網路等。不過大部分系統掉用都提供非Exception的錯誤處理。不過程式開發各不相同,用不用Exception可能還是需要大家自行決定。
Aear個人觀點是能不用Exception,就不用Exception,但是應該用Exception的時候,一定不要省。比如constructor裡。
============ Exception的用法 ============
要使用Exception, 要麼用系統的Exception的類,要麼定義自己的類。在定義自己類的時候,可以繼承STD裡邊的Exception類,也可以建立自己新的類。比如:
class ExceptionBase{
...
};
class ExceptionDerived : public ExceptionBase{
...
};
需要注意的是,通常定義自己的Exception類的時候,都要有一個公用的Base Exception Class, 這樣能夠保證寫代碼的時候catch所有的你自訂的Exception,比如:
try {
...
}catch( ExceptionDerived & e ) {
...
}catch( ExceptionBase & e ) {
// Catch 其他的Exception, 這樣的設計即使今後添加新的Exception,只要
// 是從ExceptionBase繼承來的,都會被catch到。
}catch( ... ) {
// 這裡最好再加上 catch(...)來catch所有的exception,防止有未catch的 // exception. 因為如果有unexpected exception, C++的預設動作是直接
// 終止程式的運行。
};
============ Exception in Constructor ============
如果一個Constructor產生exception而且沒有被程式catch到的話,那麼這個object的建立就會失敗, 比如:
class MemoryBlock {
private:
void * _pMem;
public:
MemoryBlock ( UINT32 size )
{
_pMem = new char[size];
};
....
};
MemoryBlock myMemory(100000000000000000000000000);
如果new在分配記憶體的過程中throw一個Exception ,通常是 bad_alloc,那麼myMemory的建立就會失敗,以後任何對 myMemory的成員訪問,都是非法的,會導致程式的崩潰。
讓我們看看另一中寫法:
class MemoryBlock {
private:
void * _pMem;
public:
MemoryBlock ( UINT32 size ) :
_pMem(new char[size])
{ };
....
};
上面也是合法的,不過會產生同樣的問題。但是區別在於如果在代碼中catch到exception,那麼第一種寫法,能夠保證object被建立,而第二種寫法不能。比如:
// MemoryBlock 能夠被建立
MemoryBlock ( UINT32 size )
{
try {
_pMem = new char[size];
} catch(...) {}
};
// MemoryBlock 建立失敗
MemoryBlock ( UINT32 size )
try
: _pMem(new char[size])
{ } catch(...) {};
============ Exception in Destructor ============
其實對於Destructor來說就一句話,不能在Destructor中Throw Exception。原因很簡單,因為通常Destructor要麼在Delete Object中掉用,要麼在已經Throw了Exception的時候,由系統掉用。如果在Throw Exception的情況下再Throw Exception的話,那麼程式就會強制終止。
============ Exception in Operator ============
這個是比較麻煩的,通常的Exception的處理有好幾個層級, Basic, Strong, Nofail.我們這裡只說下Strong Exception Safety。 下面是個例子:
class X {
...
private:
void * _pMem1;
UINT32 _pMemSize1;
void * _pMem2;
UINT32 _pMemSize2;
public:
X& operator = ( const X & xo )
{
if( _pMem1 ) delete _pMem1;
if( _pMem2 ) delete _pMem2;
_pMem1 = new char[xo._pMemSize1];
_pMem2 = new char[xo._pMemSize1];
...
};
};
這裡如果 _pMem2 = new char[xo._pMemSize1]; Throw一個Exception,那麼X只是被Copy了一半。狀態是不完整的。但是原來在pMem1&2中的資料已經消失了。如果是Strong Exception Safety,那麼要求如果throw excpetion,那麼class的資料應該恢複在之前的狀態,比如經典的exception safe operator = 如下:
X& operator = ( const X & xo )
{
X temp(xo);
swap( *this, temp );
return *this;
};
swap是交換*this 和 Temp的所有資料。通常我們能夠保證這個過程沒有任何exception的產生。因此即使 temp(xo) throw一個exception, 也不會影響當前類的任何狀態變化。
============ RAII ============
最後說一種不使用Exception而能保證沒有Resource Leakage的技術。那就是 Resource Aquisition Is Initialization ( RAII ). 其原理很簡單,就是C++標準保證一個被成功建立的 Object,無論任何情況下(即使是在Throw exception ), 它的 Destructor都會被掉用。 因此,我們可以用一個object 的constructor 來擷取資源,用Destructor來釋放資源。下面舉個最簡單的應用,thread 的 asynchronization:
class CriticalSection {
public:
CriticalSection( CRTICIAL_SECTION *pCs ) :
_pCs(pCS)
{ EnterCriticalSection( _pCS ) };
~CriticalSection( )
{ LeaveCriticalSection( _pCS ) };
private:
CRTICIAL_SECTION * _pCs;
};
通常我們使用Critical Section的時候,用下列方式:
void threadXX( CRTICIAL_SECTION * pCs)
{
EnterCriticalSection( pCS );
void * pTemp = new char[100000000];
LeaveCriticalSection( pCS );
}
問題是如果 void * pTemp = new char[100000000]; Throw一個 bad_alloc,那麼 LeaveCriticalSection( pCS );就不會被掉用而直接返回,很容易導致死結。類似的代碼在遊戲伺服器端的設計是很常見的,正確的做法是使用上面定義的類:
void threadXX( CRTICIAL_SECTION * pCs)
{
CriticalSection temp( pCS );
void * pTemp = new char[100000000];
}
由於即使throw exception, C++保證temp的destructor一定會被調用。因此不會產生死結的情況。
============ 其他 ============
比如下面的代碼是很容易產生問題的:
function( new char[100], new char[300] );
如果new char[300]throw exception,那麼 new char[100]很有可能就不會被釋放。
推薦使用auto_ptr或者boost中的Shared_ptr,特別是在class 的initialization list 中, 比如下列做法不使用catch exception也不會產生記憶體泄露:
class X{
X() :
_ptr1(new XXX() ),
_ptr2(new XXX() )
{};
private:
auto_ptr<void *> _ptr1;
auto_ptr<void *> _ptr2;
}
Destructor中不需要catch exception,因為destructor主要是調用其他的destructor,沒有任何的destructor會throw exception的,所以沒必要catch.