Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C
Article

A Generic Approach to Debug Tracing and Logging

Rate me:
Please Sign up or sign in to vote.
4.09/5 (7 votes)
28 Mar 2008GPL36 min read 36.1K   230   32   5
A generic tracing library implementation.

Introduction

Debug tracing and logging is a very common technique in the programming world. We can switch on and off the debug/logging messages by just defining a macro. However, most of the tracing/logging utilities provided by vendors lack type check and extendibility. In this article, we will introduce a new approach to implementing a tracing/logging utility, which can be customized in output format, threading model, output stream, and object life time, by using generic programming techniques.

Type checking

The syntax of a tracer should be like:

C++
TRACE("any","thing","you","want"); 

We can not make assumptions of what our customers want to trace, may be an integer, may be a self defined class instance. MFC defines the TRACE macro as:

C++
void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...);
...
#define TRACE  ::AfxTrace

The major problem with this macro is type checking. Once we define a function like:

C++
void AFX_CDECL AfxTrace(LPCTSTR lpszFormat, ...);

there is not much C++ left. Users have to determine the parameter types by themselves. Furthermore, passing objects to it will result in undefined behavior. A C++ way to do it is templates. By overloading template functions like:

template<typename T0> 
inline void Trace(const T0& p0);
template<typename T0,typename T1>
inline void Trace(const T0& p0,const T1& p1); 
...

we can achieve the same effect as ellipses, but in a type safe way. Yes, there is a limitation of how many parameters you can pass to Trace. But codes are written by hands, it is very rare to want to pass over 15 parameters to a single function. We can assume 15 of those functions should be enough.

Output stream

Sometimes, we can’t use the std output for our debug tracing (a window application). Or we want to print our tracing messages to a file. There must be a way for our clients to use their own output stream. This is easily achieved by using templates:

C++
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_;
};

But the story does not end; someone has to manage the instance of Tracer. Since our goal is be able to turn on/off by just defining a single macro, letting the clients manage the instance is obviously unacceptable. The Singleton pattern might be perfect here; it allows us to retrieve the instance by just knowing the class name. Well, once we go into the Singleton pattern, we find that it is not that easy as it seems to be. We have to consider the dead reference problem and the multithread safety problem (for detailed descriptions, see: <<Modern C++ Design: Generic Programming and Design Patterns Applied>>). The Loki library has already provided a full-fledged generic implementation of the Singleton pattern, which has the following declaration:

C++
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; 

The first template parameter tells the Singleton holder what to hold, the second one tells how it will be created, the third tells how to respond to the dead reference problem, and the last two tell which threading model to use. Since the default Creation Policies defined by the Loki library require classes have default constructors, obviously, our class doesn’t fullfill the requirement, and we have to define our own Create Policy.

We want to offer our client the capability to customize the output stream; we would like to define our Create Policy like this:

C++
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;
    } 
};

The StreamCreatePolicy class offers a function named CreateOutputStream to return an instance of the customizable output stream. Therefore, by passing different template parameters, clients can customize the output stream used by the tracer. But, there is problem with this class; that is, Loki’s Singleton CreatePolicy class is defined like:

C++
template <class> class CreatePolicy;

We can’t pass our own create policy to Loki’s singleton holder! The template parameter taken by the CreatePolicy here tells it what to create. This is actually information we already know; what we need to know here is what type of output stream we are going to use and how to get an instance of it. So here, we need a builder class to build the tracer:

C++
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; 
};

Now, we build our tracer by passing the template parameter to the class Tracer_Builder by using the following code:

C++
typedef Tracer_Builder< 
    OstreamType,  
    StreamCreatePolicy, 
    LifetimePolicy, 
    ThreadingModel, 
    MutexPolicy 
>::Tracer MyTracer;
MyTracer::Instance().Trace("anything","you","want");

Output format

Our little tracing utility library is now fully functional. But, there are still some things we can improve. Sometimes, we may want the tracer to automatically add a new line after each time it prints. Sometimes, we may want to use a “,” to delimit the elements printed by the tracer. So, we have to offer our clients a way to customize the tracer output format. To achieve this, we can let them define their own manipulator for the output streams.

C++
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;
   }
   ... 
};

So, by implementing a Header, Delemiter, and Footer functions, clients now can fully control the output format. And also, we have to modify our Tracer_Builder class as well, make it take this new policy class, and pass it to the Tracer. Because it is just a simple change, the code is not listed here.

Multithread

As multithread is so common in programming, our little lib must offer something to deal with it. The Loki library has provided three levels of threading models: single thread, object level lock, and class level lock. Each of those threading model classes have an inner definition class named Lock, which provides the locking service. Since our class uses the Singleton pattern, the object level lock does not making any sense. Our class will use two levels of threading models: single thread and class level lock. With our new threading model policy, our Tracer class will look like the following:

C++
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;
   }
   ... 
};

Also, we have to make some changes to our Tracer_Builder class. The final version of our Tracer_Builder would look like:

C++
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; 
};

Macros

We have two goals in this section. One is to allow clients to switch the trace output by using a macro. The other one is to allow clients to customize their own tracer by using a macro.

The first goal is very easy to achieve by using the following code:

#ifdef TRACE_DISABLED
#define XTrace  
#else
#define XTrace \
Tracer_Builder<>::Tracer::Instance().Trace
#endif

For the second goal, we can apply a simple strategy: if clients have defined their own Tracer_Builder, we use it; otherwise, we use default Tracer_Builder. The code would look like the following:

#ifndef
TRACER_BUILDER
#define
TRACER_BUILDER Tracer_Builder<>
#endif

And the macro XTrace is changed to:

#define
XTrace TRACER_BUILDER::Tracer::Instace().Trace 

But there is a problem: our library allows clients to use and define their own policy classes as an extension of the library. If clients want to use them, they must define their own policy classes before the header, which is out of C++ conventions. Therefore, we have to separate the notification to the library and the definition of the Tracer Builder. The final macros would look like:

#ifdef TRACE_DISABLED
#define XTrace 
#else
#ifndef
CUSTOMIZE_TRACER_BUILDER
#define TRACER_BUILDER
Tracer_Builder<>
#endif
#define XTrace \
TRACER_BUILDER::Tracer::Instance().Trace
#endif

Now, clients can customize their code like:

#define
CUSTOMIZE_TRACER_BUILDER
#include <Tracer.h>
#define TRACER_BUILDER \ 
    Tracer_Builder<OstreamType,\ 
    StreamCreatePolicy,\
    LifetimePolicy,\
    ThreadingModel, \
    MutexPolicy>

How to use the code

Check out the source code, it comes with several examples.

Conclusion

By solving the type checking problem, output stream customization problem, output stream formatting customization problem, and the multithread safety problem, we now have an open ended generic implementation of a tracing and logging utility.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionCreateUsingFile? Pin
faysou27-Jun-11 11:09
faysou27-Jun-11 11:09 
AnswerRe: CreateUsingFile? Pin
faysou29-Jun-11 1:39
faysou29-Jun-11 1:39 
GeneralFYI Pin
balazs_hideghety30-Mar-08 9:03
balazs_hideghety30-Mar-08 9:03 
GeneralRe: FYI Pin
Stefan_Lang1-Apr-08 1:46
Stefan_Lang1-Apr-08 1:46 
GeneralRe: FYI Pin
TommyTooth1-Apr-08 2:39
TommyTooth1-Apr-08 2:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.