POCO C++庫學習和分析 -- 異常、錯誤處理、調試
1. 異常處理
C++同C語言相比,提供了異常機制。通過使用try,catch關鍵字可以捕獲異常,這種機制使得程序員在程序異常發生時,可以通過判斷異常類型,來決定程序是否繼續執行,並在程序結束之前優雅的釋放各類資源。當然對於C++的異常機制也存在著很多的爭議。在這裡,並不對此展開討論,只介紹一下Poco中的異常類。
Poco中的異常類:
1. 所有的異常類都是Poco::Exception的子類。
2. Poco::Exception繼承自std::exception類。
3. Foundation庫中涉及的異常類,包括了下面一些:
a) Poco::LogicException類負責處理程序錯誤,包括了:
AssertionViolationException
NullPointerException
NullValueException
BugcheckException
InvalidArgumentException
NotImplementedException
RangeException
IllegalStateException
InvalidAccessException
SignalException
UnhandledException
b) Poco::ApplicationException類負責處理應用程序相關的錯誤,即使用Poco庫的用戶自定義異常。
c) Poco::RuntimeException類負責處理程序運行時的錯誤,包括了:
RuntimeException
NotFoundException
ExistsException
TimeoutException
SystemException
RegularExpressionException
LibraryLoadException
LibraryAlreadyLoadedException
NoThreadAvailableException
PropertyNotSupportedException
PoolOverflowException
NoPermissionException
OutOfMemoryException
DataException
DataFormatException
SyntaxException
CircularReferenceException
PathSyntaxException
IOException
ProtocolException
FileException
FileExistsException
FileNotFoundException
PathNotFoundException
FileReadOnlyException
FileAccessDeniedException
CreateFileException
OpenFileException
WriteFileException
ReadFileException
UnknownURISchemeException
成員函數及數據定義:
1. Poco::Exception包括了一個名字,這是一個靜態的字符串,用來描述異常本身。比如說LogicException名字為"Logic exception",TimeoutException名字為"Timeout"。
2. Poco::Exception還包含了一個字符串消息,這是用來進一步描述異常的。使用的的人可以在運行時定義它。比如都是LogicException異常,函數一處拋出異常時可定義為"Function1",函數二處拋出時異常時可定義為用"Function2",它可以用來說明異常發生的具體位置和原因。
3. 一個可選的嵌套異常類
4. 構造函數:
a) 可以使用0個,1個或2個字符串參數來構造異常。在Poco::Exception內部存儲的時候,第二個字符串會使用字符":"和第一個字符串串聯。
b) 構造時如果使用了字符串和嵌套異常的方式,嵌套異常會被復制一份。
5. Poco::Exception支持拷貝和賦值運算符
6. const char* name()
返回異常的名稱
7. const std::string& message()
返回在構造時傳入的消息字符串
8. std::string displayText() const
同時返回異常名字和消息字符串,中間使用": "分隔
9. const Exception* nested() const
如果存在嵌套異常的話,返回之歌指向嵌套異常的指針,否則返回0
10. Exception* clone() const
返回一個異常的拷貝
11. void rethrow() const
重新拋出異常
定義自己的異常:
因為從Poco::Exception繼承,去定義自己的異常時,工作非常的枯燥且重復(用戶需要重載大量的虛函數),在庫中提供了兩個宏來完成這個工作:
POCO_DECLARE_EXCEPTION:用來申明異常宏
POCO_IMPLEMENT_EXCEPTION: 用來定義異常宏的執行體
兩個宏分別定義如下:
[cpp]
// MyException.h
#include "Poco/Exception.h"
POCO_DECLARE_EXCEPTION(MyLib_API, MyException, Poco::Exception)
// MyException.h
#include "Poco/Exception.h"
POCO_DECLARE_EXCEPTION(MyLib_API, MyException, Poco::Exception)
[cpp]
// MyException.cpp
#include "MyException.h"POCO_IMPLEMENT_EXCEPTION(MyException, Poco::Exception,"Something really bad happened...")
// MyException.cpp
#include "MyException.h"POCO_IMPLEMENT_EXCEPTION(MyException, Poco::Exception,"Something really bad happened...")
宏展開分別為:
[cpp]
// MyException.h
#include "Poco/Exception.h"
POCO_DECLARE_EXCEPTION(MyLib_API, MyException, Poco::Exception)
class MyLib_API MyException: public Poco::Exception
{
public:
MyException();
MyException(const std::string& msg);
MyException(const std::string& msg, const std::string& arg);
MyException(const std::string& msg, const Poco::Exception& nested);
MyException(const MyException& exc);
~MyException();
MyException& operator = (const MyException& exc);
const char* name() const;
...
};
// MyException.h
#include "Poco/Exception.h"
POCO_DECLARE_EXCEPTION(MyLib_API, MyException, Poco::Exception)
class MyLib_API MyException: public Poco::Exception
{
public:
MyException();
MyException(const std::string& msg);
MyException(const std::string& msg, const std::string& arg);
MyException(const std::string& msg, const Poco::Exception& nested);
MyException(const MyException& exc);
~MyException();
MyException& operator = (const MyException& exc);
const char* name() const;
...
};
[cpp]
// MyException.cpp
#include "MyException.h"
POCO_IMPLEMENT_EXCEPTION(MyException, Poco::Exception,
"Something really bad happened...")
...
const char* MyException::name() const throw()
{
return "Something really bad happened...";
}
...
// MyException.cpp
#include "MyException.h"
POCO_IMPLEMENT_EXCEPTION(MyException, Poco::Exception,
"Something really bad happened...")
...
const char* MyException::name() const throw()
{
return "Something really bad happened...";
}
...
下面是一個例子:
[cpp]
#include "Poco/Exception.h"
#include <iostream>
int main(int argc, char** argv)
{
Poco::Exception* pExc = 0;
try
{
throw Poco::ApplicationException("just testing");
}
catch (Poco::Exception& exc)
{
pExc = exc.clone();
}
try
{
pExc->rethrow();
}
catch (Poco::Exception& exc)
{
std::cerr << exc.displayText() << std::endl;
}
delete pExc;
return 0;
}
#include "Poco/Exception.h"
#include <iostream>
int main(int argc, char** argv)
{
Poco::Exception* pExc = 0;
try
{
throw Poco::ApplicationException("just testing");
}
catch (Poco::Exception& exc)
{
pExc = exc.clone();
}
try
{
pExc->rethrow();
}
catch (Poco::Exception& exc)
{
std::cerr << exc.displayText() << std::endl;
}
delete pExc;
return 0;
}
2. 斷言
POCO庫中提供了一些斷言的宏來進行運行時檢查,這些斷言能夠提供出錯代碼的行號和文件信息。
1. Debugger::_assert(cond)
如果cond ≠ true時,拋出一個AssertionViolationException異常。
2. poco_assert_dbg(cond)
同poco_assert類似,但是只在debug模式下起作用
3. poco_check_ptr(ptr)
如果ptr為空,則拋出NullPointerException異常
4. poco_bugcheck(), poco_bugcheck_msg(string)
拋出BugcheckException異常
POCO的斷言類在debug調試模式下(比如在Visual C++)中時,會觸發一個breakpoint。比如:
[cpp]
void foo(Bar* pBar)
{
poco_check_ptr (pBar);
...
}
void baz(int i)
{
poco_assert (i >= 1 && i < 3);
switch (i)
{
case 1:
...
break;
case 2:
...
break;
default:
poco_bugcheck_msg("i has invalid value");
}
}
void foo(Bar* pBar)
{
poco_check_ptr (pBar);
...
}
void baz(int i)
{
poco_assert (i >= 1 && i < 3);
switch (i)
{
case 1:
...
break;
case 2:
...
break;
default:
poco_bugcheck_msg("i has invalid value");
}
}
這主要是因為Poco中的斷言類是通過Poco::Debugger去實現的,在Poco::Debugger底層調用了不同操作系統的API,去判斷程序是否處於調試狀態。如VC下,調用了
[cpp]
BOOL WINAPI IsDebuggerPresent(VOID);
VOID WINAPI DebugBreak(VOID);
BOOL WINAPI IsDebuggerPresent(VOID);
VOID WINAPI DebugBreak(VOID);
3. NDC(Nested Diagnostic Context)
3.1 概述
NestedDiagnosticContext是為了多線程診斷而設計的。我們在寫程序時,一般都需要同時處理多個線程。為了更加便捷的處理多線程情況,為每個線程產生各自的日志。Neil Harrison 在他的書中"Patterns for Logging Diagnostic Messages," in Pattern Languages of Program Design 3, edited by R. Martin, D. Riehle, and F. Buschmann (Addison-Wesley, 1997) 中提出了一個方法。獨特地標記每個日志請求,用戶把上下文信息送入NDC,NDC是 Nested Diagnostic Context的縮寫。在這本書裡提到了3種日志方法,分別是:
1. DiagnosticLogger
分離日志和程序其他模塊
2. TransactionalBuckets
事務桶,為事務單獨建立日志
3. TypedDiagnostics
類型化診斷,為所有的診斷信息提供統一的展現
我們還是回到Poco中的NDC上。在Poco中和NDC相關的內容包括了,NestedDiagnosticContext類,NDCScope類,宏poco_ndc和poco_ndc_dbg。其中NestedDiagnosticContext類維護一個NDC對象,其中包括了上下文的棧信息,有函數方法名,源文件代碼文件名,行號。宏poco_ndc(func) or poco_ndc_dbg(func)申明了一個NDCScope對象。而NDCScope對象則完成了上下文的入棧工作。下面是一個例子:
[cpp]
#include "Poco/NestedDiagnosticContext.h"
#include <iostream>
void f1()
{
poco_ndc(f1);
Poco::NDC::current().dump(std::cout);
}
void f2()
{
poco_ndc(f2);
f1();
}
int main(int argc, char** argv)
{
f2();
return 0;
}
#include "Poco/NestedDiagnosticContext.h"
#include <iostream>
void f1()
{
poco_ndc(f1);
Poco::NDC::current().dump(std::cout);
}
void f2()
{
poco_ndc(f2);
f1();
}
int main(int argc, char** argv)
{
f2();
return 0;
}
3.2 實現
3.2.1 線程本地存儲
在Poco中實現時,用了一些小技巧,即線程本地存儲。我們來看Poco中TLS的類圖:
CurrentThreadHolder類是TLS實現的具體類,在每個Thread對象中包含了一個CurrentThreadHolder對象。Thread創建的時候,CurrentThreadHolder會調用不同操作系統的API函數,獲取並保存一個固定槽位,用於保存Thread對象的指針。
每個Thread對象中還包含了一個ThreadLocalStorage對象。ThreadLocalStorage類用於保存具體的線程信息數據,它是一個TLSSlot對象的集合。通過泛型實現TLSSlot後,ThreadLocalStorage可用於保存任何數據的。
使用了TLS技術後,調用Thread的靜態函數current可以獲取到每個線程對象Thread的指針,然後再通過這個Thread對象的指針,可以獲取到ThreadLocalStorage對象,並最終獲取或保存數據於TLSSlot中。
通過類的靜態函數獲取類實例的指針,在C++中是不存在的,這需要操作系統支持,只有Thread對象才能做到這一點。
3.2.2 NDC
在來看一張Poco中NDC類的類圖:
使用者通過調用宏poco_ndc和poco_ndc_dbg,來構建一個NDCScope對象。宏定義如下:
[cpp]
#define poco_ndc(func) \
Poco::NDCScope _theNdcScope(#func, __LINE__, __FILE__)
#if defined(_DEBUG)
#define poco_ndc_dbg(func) \
Poco::NDCScope _theNdcScope(#func, __LINE__, __FILE__)
#else
#define poco_ndc_dbg(func)
#endif
#define poco_ndc(func) \
Poco::NDCScope _theNdcScope(#func, __LINE__, __FILE__)
#if defined(_DEBUG)
#define poco_ndc_dbg(func) \
Poco::NDCScope _theNdcScope(#func, __LINE__, __FILE__)
#else
#define poco_ndc_dbg(func)
#endif
NDCScope實現了診斷信息上下文的入棧出棧工作,它通過調用NestedDiagnosticContext類的靜態函數current實現了此功能。其定義如下:
[cpp]
inline NDCScope::NDCScope(const std::string& info)
{
NestedDiagnosticContext::current().push(info);
}
inline NDCScope::NDCScope(const std::string& info, int line, const char* filename)
{
NestedDiagnosticContext::current().push(info, line, filename);
}
inline NDCScope::~NDCScope()
{
NestedDiagnosticContext::current().pop();
}
inline NDCScope::NDCScope(const std::string& info)
{
NestedDiagnosticContext::current().push(info);
}
inline NDCScope::NDCScope(const std::string& info, int line, const char* filename)
{
NestedDiagnosticContext::current().push(info, line, filename);
}
inline NDCScope::~NDCScope()
{
NestedDiagnosticContext::current().pop();
}
NestedDiagnosticContext類的current()是個靜態函數,其定義如下:
[cpp]
namespace
{
static ThreadLocal<NestedDiagnosticContext> ndc;
}
NestedDiagnosticContext& NestedDiagnosticContext::current()
{
return ndc.get();
}
namespace
{
static ThreadLocal<NestedDiagnosticContext> ndc;
}
NestedDiagnosticContext& NestedDiagnosticContext::current()
{
return ndc.get();
}
而ThreadLocal是一個輔助類,用於獲取線程對象的本地存儲信息或者是主線程的本地存儲信息。
[cpp]
template <class C>
class ThreadLocal
/// This template is used to declare type safe thread
/// local variables. It can basically be used like
/// a smart pointer class with the special feature
/// that it references a different object
/// in every thread. The underlying object will
/// be created when it is referenced for the first
/// time.
/// See the NestedDiagnosticContext class for an
/// example how to use this template.
/// Every thread only has access to its own
/// thread local data. There is no way for a thread
/// to access another thread's local data.
{
typedef TLSSlot<C> Slot;
public:
ThreadLocal()
{
}
~ThreadLocal()
{
}
C* operator -> ()
{
return &get();
}
C& operator * ()
/// "Dereferences" the smart pointer and returns a reference
/// to the underlying data object. The reference can be used
/// to modify the object.
{
return get();
}
C& get()
/// Returns a reference to the underlying data object.
/// The reference can be used to modify the object.
{
TLSAbstractSlot*& p = ThreadLocalStorage::current().get(this);
if (!p) p = new Slot;
return static_cast<Slot*>(p)->value();
}
private:
ThreadLocal(const ThreadLocal&);
ThreadLocal& operator = (const ThreadLocal&);
};
template <class C>
class ThreadLocal
/// This template is used to declare type safe thread
/// local variables. It can basically be used like
/// a smart pointer class with the special feature
/// that it references a different object
/// in every thread. The underlying object will
/// be created when it is referenced for the first
/// time.
/// See the NestedDiagnosticContext class for an
/// example how to use this template.
/// Every thread only has access to its own
/// thread local data. There is no way for a thread
/// to access another thread's local data.
{
typedef TLSSlot<C> Slot;
public:
ThreadLocal()
{
}
~ThreadLocal()
{
}
C* operator -> ()
{
return &get();
}
C& operator * ()
/// "Dereferences" the smart pointer and returns a reference
/// to the underlying data object. The reference can be used
/// to modify the object.
{
return get();
}
C& get()
/// Returns a reference to the underlying data object.
/// The reference can be used to modify the object.
{
TLSAbstractSlot*& p = ThreadLocalStorage::current().get(this);
if (!p) p = new Slot;
return static_cast<Slot*>(p)->value();
}
private:
ThreadLocal(const ThreadLocal&);
ThreadLocal& operator = (const ThreadLocal&);
};
到這裡Poco中所有的NDC流程都被打通了,用戶終於可以實現按線程打印日志信息了。