通用调试跟踪和日志记录方法






4.09/5 (7投票s)
一个通用的跟踪库实现。
引言
调试跟踪和日志记录是编程界中一项非常常见的技术。我们只需定义一个宏就可以打开和关闭调试/日志消息。然而,供应商提供的绝大多数跟踪/日志实用程序都缺乏类型检查和可扩展性。在本文中,我们将介绍一种实现跟踪/日志实用程序的新方法,该方法可以使用泛型编程技术来定制输出格式、线程模型、输出流和对象生命周期。
类型检查
跟踪器的语法应该是这样的
TRACE(“any”,”thing”,”you”,”want”);
我们不能假设我们的客户想跟踪什么,可能是整数,可能是自定义类的实例。MFC 定义了 TRACE
宏,如下所示:
void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...);
...
#define TRACE ::AfxTrace
此宏的主要问题是类型检查。一旦我们定义了一个函数,例如:
void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...);
C++ 剩下的不多了。用户必须自己确定参数类型。此外,将对象传递给它将导致未定义的行为。一种 C++ 的方法是使用模板。通过重载模板函数,例如:
template<typename T0> inline void Trace(const T0& p0); template<typename T0,typename T1> inline void Trace(const T0& p0,const T1& p1); ...
我们可以实现与省略号相同的效果,但方式是类型安全的。是的,传递给 Trace
的参数数量有限制。但代码是手动编写的,很少有人想将超过 15 个参数传递给单个函数。我们可以假设这些函数中的 15 个应该足够了。
输出流
有时,我们无法使用标准输出进行调试跟踪(例如窗口应用程序)。或者我们想将跟踪消息打印到文件。必须有一种方法让我们的客户使用他们自己的输出流。这可以通过使用模板轻松实现:
template<class OstreamType>
class Tracer{
public:
Tracer(OstreamType& ostream):ostream_(ostream){}
template<typename T0>
inline void Trace(const T0& p0){
ostream_<<p0;
}
template<typename T0,typename T1>
inline void Trace(const T0&p0,const T1& p1){
ostream_<<p0<<p1;
}
...
private:
OstreamTyep& ostream_;
};
但故事并未就此结束,有人必须管理 Tracer
的实例。由于我们的目标是只需定义一个宏即可打开/关闭,因此让客户管理实例显然是不可接受的。单例模式在这里可能很完美;它允许我们通过知道类名来检索实例。好吧,一旦我们进入单例模式,就会发现它并不像看起来那么容易。我们必须考虑悬空引用问题和多线程安全问题(有关详细描述,请参阅:《现代 C++ 设计:泛型编程和设计模式应用》)。Loki 库已经提供了单例模式的完整泛型实现,其声明如下:
template<
typename T,
template <class> class CreationPolicy =
CreateUsingNew,
template <class> class LifetimePolicy =
DefaultLifetime,
template <class, class> class ThreadingModel =
LOKI_DEFAULT_THREADING_NO_OBJ_LEVEL,
class MutexPolicy = LOKI_DEFAULT_MUTEX
>class
SingletonHolder;
第一个模板参数告诉 Singleton holder 持有什么,第二个参数告诉它将如何创建,第三个参数告诉它如何响应悬空引用问题,最后两个参数告诉它使用哪种线程模型。由于 Loki 库定义的默认创建策略要求类具有默认构造函数,显然,我们的类不满足要求,我们必须定义自己的创建策略。
我们希望为客户提供自定义输出流的功能;我们想定义我们的创建策略如下:
template<
class OstreamType,
template<class> class StreamCreatePolicy
>struct CreateUsingNew:
public StreamCreatePolicy<OstreamType>{
static Tracer<OstreamType>*
Create(){
return new Tracer<OstreamType>
(CreateOutputStream());
}
static void Destroy(Tracer<OstreamType>*p){
delete p;
}
};
StreamCreatePolicy
类提供了一个名为 CreateOutputStream
的函数,用于返回可自定义的输出流实例。因此,通过传递不同的模板参数,客户可以自定义跟踪器使用的输出流。但是,此类存在一个问题;那就是 Loki 的 Singleton CreatePolicy
类定义如下:
template <class> class CreatePolicy;
我们无法将自己的创建策略传递给 Loki 的 singleton holder!这里 CreatePolicy
采用的模板参数告诉它要创建什么。这实际上是我们已经知道的信息;我们这里需要知道的是我们要使用的输出流类型以及如何获取其实例。因此,这里我们需要一个构建器类来构建跟踪器。
template<
class OstreamType = std::ostream,
template <class> class StreamCreatePolicy =
CreateUsingStdOutput,
template <class> class LifetimePolicy =
Loki::DefaultLifetime,
template <class, class> class ThreadingModel =
LOKI_DEFAULT_THREADING_NO_OBJ_LEVEL,
class MutexPolicy =LOKI_DEFAULT_MUTEX
>
class Tracer_Builder{
public:
//////////////////////////////////////////////
//define what the singleton holder will hold
//////////////////////////////////////////////
typedef Private::Tracer<
// tracer we defined before
OstreamType,
> Trace_Function_Type;
/////////////////////////////////////////////
//define create policy for singleton holder
/////////////////////////////////////////////
template<class T> struct
CreateUsingNew;
template<>
struct CreateUsingNew<Trace_Function_Type>:
public StreamCreatePolicy<OstreamType>{
static Trace_Function_Type*Create(){
return new Trace_Function_Type
(CreateOutputStream());
}
static voidDestroy(Trace_Function_Type* p){
delete p;
}
};
/////////////////////////////////////////////
//define Tracer
/////////////////////////////////////////////
typedef Loki::SingletonHolder<
Trace_Function_Type,
CreateUsingNew,
LifetimePolicy,
ThreadingModel,
MutexPolicy> Tracer;
};
现在,我们通过使用以下代码将模板参数传递给 Tracer_Builder
类来构建我们的跟踪器:
typedef Tracer_Builder<
OstreamType,
StreamCreatePolicy,
LifetimePolicy,
ThreadingModel,
MutexPolicy
>::Tracer MyTracer;
MyTracer::Instance().Trace(“anything”,”you”,”want”);
输出格式
我们小巧的跟踪实用程序库现在已 fully functional。但仍有一些可以改进的地方。有时,我们可能希望跟踪器在每次打印后自动添加一个新行。有时,我们可能希望使用“,”来分隔跟踪器打印的元素。因此,我们必须为客户提供自定义跟踪器输出格式的方法。为了实现这一点,我们可以让他们为输出流定义自己的操纵器。
template<class OstreamType>
class MyFormat{
protected:
static OstreamType& Header(OstreamType& ostream){
return ostream;
}
static OstreamType& Delimiter(OstreamType& ostream){
return ostream<<" ";
}
static OstreamType& Footer(OstreamType& ostream){
return ostream<<std::endl;
}
};
template<
class OstreamType,
template <class> class FormatPolicy =DefaultFormat
>
class Tracer:public
FormatPolicy<OstreamType>{
public:
Trace_Function(OstreamType&ostream):
ostream_(ostream){}
template<typename T0>
inline void Trace(const T0& p0){
ostream_<<Header<<p0<<Footer;
}
template<typename T0,typename T1>
inline void Trace(const T0& p0,const T1& p1){
ostream_<<Header<<p0<<Delimiter<<p1<<Footer;
}
...
};
因此,通过实现 Header
、Delimiter
和 Footer
函数,客户现在可以完全控制输出格式。同时,我们也必须修改我们的 Tracer_Builder
类,使其接受这个新的策略类,并将其传递给 Tracer
。由于这只是一个简单的更改,因此此处不列出代码。
多线程
由于多线程在编程中非常普遍,我们的小型库必须提供一些功能来处理它。Loki 库提供了三个级别的线程模型:单线程、对象级别锁和类级别锁。这些线程模型类中的每一个都有一个名为 Lock
的内部定义类,它提供锁定服务。由于我们的类使用单例模式,因此对象级别的锁没有意义。我们的类将使用两个级别的线程模型:单线程和类级别锁。通过我们的新线程模型策略,我们的 Tracer
类将如下所示:
template<
class OstreamType,
template <class> class FormatPolicy =DefaultFormat,
template <class, class> class ThreadingModel =
LOKI_DEFAULT_THREADING_NO_OBJ_LEVEL,
class MutexPolicy = LOKI_DEFAULT_MUTEX
>
class Tracer:public FormatPolicy<OstreamType>{
public:
typedef typename ThreadingModel<Trace_Function,
MutexPolicy>::Lock Lock;
Trace_Function(OstreamType&ostream):
ostream_(ostream){}
template<typename T0>
inline void Trace(const T0& p0){
Lock guard;
ostream_<<Header<<p0<<Footer;
}
template<typename T0,typename T1>
inline void Trace(const T0& p0,const T1& p1){
Lock guard;
ostream_<<Header<<p0<<Delimiter<<p1<<Footer;
}
...
};
此外,我们必须对 Tracer_Builder
类进行一些更改。Tracer_Builder
的最终版本将如下所示:
template<
class OstreamType = std::ostream,
template <class> class FormatPolicy =
DefaultFormat,
template <class> class StreamCreatePolicy =
CreateUsingStdOutput,
template <class> class LifetimePolicy =
Loki::DefaultLifetime,
template <class, class> class ThreadingModel =
LOKI_DEFAULT_THREADING_NO_OBJ_LEVEL,
class MutexPolicy = LOKI_DEFAULT_MUTEX
>
class Tracer_Builder{
public:
//////////////////////////////////////////////
//define what the singleton holder will hold
//////////////////////////////////////////////
typedef Private::Tracer<
//the tracer we defined before
OstreamType,
FormatPolicy,
ThreadingModel,
MutexPolicy
> Trace_Function_Type;
/////////////////////////////////////////////
//define create policy for singleton holder
/////////////////////////////////////////////
template<class T>
struct CreateUsingNew;
template<>
struct CreateUsingNew<Trace_Function_Type>:
public StreamCreatePolicy<OstreamType>{
static Trace_Function_Type* Create(){
return new Trace_Function_Type
(CreateOutputStream());
}
static void Destroy(Trace_Function_Type* p){
delete p;
}
};
/////////////////////////////////////////////
//define Tracer
/////////////////////////////////////////////
typedef Loki::SingletonHolder<
Trace_Function_Type,
CreateUsingNew,
LifetimePolicy,
ThreadingModel,
MutexPolicy
> Tracer;
};
宏
本节有两个目标。一是允许客户使用宏切换跟踪输出。二是允许客户使用宏自定义自己的跟踪器。
第一个目标很容易通过以下代码实现:
#ifdef TRACE_DISABLED #define XTrace #else #define XTrace \ Tracer_Builder<>::Tracer::Instance().Trace #endif
对于第二个目标,我们可以采用一个简单的策略:如果客户定义了自己的 Tracer_Builder
,我们就使用它;否则,我们就使用默认的 Tracer_Builder
。代码如下所示:
#ifndef TRACER_BUILDER #define TRACER_BUILDER Tracer_Builder<> #endif
并且宏 XTrace
被更改为:
#define
XTrace TRACER_BUILDER::Tracer::Instace().Trace
但存在一个问题:我们的库允许客户使用和定义自己的策略类作为库的扩展。如果客户想使用它们,他们必须在头文件之前定义自己的策略类,这不符合 C++ 的约定。因此,我们必须将通知库和定义 Tracer Builder 分开。最终的宏将如下所示:
#ifdef TRACE_DISABLED #define XTrace #else #ifndef CUSTOMIZE_TRACER_BUILDER #define TRACER_BUILDER Tracer_Builder<> #endif #define XTrace \ TRACER_BUILDER::Tracer::Instance().Trace #endif
现在,客户可以像这样自定义他们的代码:
#define CUSTOMIZE_TRACER_BUILDER #include <Tracer.h> #define TRACER_BUILDER \ Tracer_Builder<OstreamType,\ StreamCreatePolicy,\ LifetimePolicy,\ ThreadingModel, \ MutexPolicy>
如何使用代码
请查看源代码,其中包含几个示例。
结论
通过解决类型检查问题、输出流自定义问题、输出流格式化自定义问题和多线程安全问题,我们现在拥有了一个**开放式**的通用跟踪和日志记录实用程序实现。