65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 STL 流运算符进行调试日志记录

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (13投票s)

2008年7月20日

CPOL

6分钟阅读

viewsIcon

61234

downloadIcon

1043

一个易于使用的调试记录器,通过自定义流缓冲区实现。

引言

在 C++ 程序中输出调试日志消息有许多不同的方法。有些使用 printf,另一些使用 std::coutstd::cerr。在 Windows 上,可以通过调用 OutputDebugString API 函数或使用 MFC 库中的 TRACE 宏将字符串发送到调试器。如果我们总能以相同的方式进行操作,例如使用 STL 流 operator<<,并可配置目的地,那不是很好吗?

我想编写以下代码

debuglogger << "This is a debug message: " << variable1 << std::endl;

调试记录器应该调用,例如,OutputDebugString,并将生成的流内容作为字符串。

OutputDebugString("This is a debug message: 42\n");

在浏览 Josuttis 关于 STL 的书 [1] 时,我偶然发现了流缓冲区类(第 13.13 章),它应该可以简化这项任务。流缓冲区只实现流的数据,因此您无需实现所有这些流运算符或流操作符。

构建一个类似 STL 的调试记录器

流缓冲区类

要构建一个流缓冲区,您必须从 std::basic_streambuf 派生并重写两个虚函数

virtual int overflow (int c)
virtual int sync()

当有新字符插入到已满的缓冲区中时,会调用 overflow 函数。调用 sync() 函数将缓冲区刷新到输出目的地。为了使输出目的地可配置,我们使用一个函数对象并定义一个基类,该基类只保存函数调用运算符的参数和返回类型。为了使其通用,我们使用一个模板参数作为字符类型(charwchar_t)。第一个参数保存用于输出的上下文。第二个参数保存调试消息的一行。为调试消息的每一行调用函数调用运算符。

template<class charT>
struct basic_log_function 
{
    typedef void result_type;                           
    typedef const charT * const first_argument_type;    
    typedef const charT * const second_argument_type;   
};

现在,我们为调试记录器流定义缓冲区。有三个模板参数使缓冲区对所使用的字符类型和输出目的地通用。charT 参数指定用于流的字符类型。logfunction 参数指定输出函数对象的类型,traits 类型定义 basic_string 的字符助手类。

template
<
    class charT,                                    // character type
    class logfunction,                              // logfunctor type
    class traits = std::char_traits<charT>          // character traits
>
class basic_debuglog_buf : public std::basic_streambuf<charT, traits>
{
    typedef std::basic_string<charT, traits> string_type;   
public:
    virtual ~basic_debuglog_buf();
    void setContext(const string_type &context);
protected:
    virtual int_type overflow (int_type c);
    virtual int sync();
private:
    string_type buffer_, context_;
    logfunction func_;
    void sendToDebugLog();
};

setContext 函数设置调试消息的上下文字符串。私有 sendToDebugLog 函数执行 logfunction 的函数调用运算符,传递上下文字符串和调试消息的当前行。

流类

要使用您自己的缓冲区创建流,必须将此缓冲区实例的指针传递给我们派生流类的 basic_ostream 类的构造函数。模板参数与 basic_debuglog_buf 类相同。

template
<
    class charT,                                // character type 
    class logfunction,                          // logfunction type
    class traits = std::char_traits<charT>      // character traits 
>
class basic_debuglog_stream : public std::basic_ostream<charT, traits>
{
    typedef std::basic_string<charT, traits> string_type;
    typedef basic_debuglog_buf<charT, logfunction, traits> buffer_type;
    typedef std::basic_ostream<charT, traits> stream_type;                  
    typedef std::basic_ostringstream<charT, traits> stringstream_type;      public:
    basic_debuglog_stream(const char *file = 0, int line = -1);
    basic_debuglog_stream(const string_type &context, const char *file = 0, int line = -1);
    virtual ~basic_debuglog_stream();
    void setContext(const string_type &context);
    const string_type getContext() const;
    basic_debuglog_stream &get() {return *this;}
private:
    basic_debuglog_stream(const basic_debuglog_stream &);
    basic_debuglog_stream &operator=(const basic_debuglog_stream &);
    void buildContext();
    const char *file_;    
    const int line_;
    string_type context_;
    buffer_type buf_;
};

setContext 函数根据文件名和行号(如果指定)以及给定的上下文消息构建一个上下文字符串,并将其传递给流缓冲区。上下文字符串的格式如下

[[<filename>][(<linenumber>)] : ][<context message> : ]<message text>

通过使用流构造函数的默认值,可以省略每个部分。一个完整的上下文字符串如下所示

c:\projects\testlogger\main.cpp(20) : main() : Hello debuglog!

getContext 函数从流中检索上下文消息。get 函数只返回对流对象的引用。这有助于在临时流对象上使用流运算符。

e.g. logstream().get() << "Hello world!" << std::endl;

如您所见,在流的私有部分中,禁止复制流对象。这三个类是我们调试记录器的基础;现在,让我们看看如何使用它们。

使用代码

首先,我们需要一个函数对象来定义调试消息的目的地。

log_to_win32_debugger 类

让我们从一个使用 Windows API 中的 OutputDebugString 的类开始。此函数将给定字符串发送到调试器。如果在 Visual Studio 中使用,消息将显示在输出窗口中。如果格式正确,我们可以在输出窗口中单击消息,并会自动显示消息输出的位置。为了保持通用性,我们将其作为以字符类型为参数的模板。函数调用运算符只是将上下文和输出字符串连接起来,并将结果传递给 OutputDebugString。从 basic_log_function 派生类并非真正必要;这只是一个助手,用于正确定义函数调用运算符。只需将函数调用运算符声明为

void operator()(const char * const context, const char * const output);

调试日志流来了

template<class charT>
class log_to_win32_debugger : public basic_log_function<charT>
{
    typedef std::basic_string<charT> string_type;
public:
    result_type operator()(first_argument_type context,               
                           second_argument_type output)
    {
        string_type s(context);
        s += output;
        OutputDebugString(s.c_str());
    }
};

现在,我们准备好定义一个具体的调试日志记录类型

typedef 
basic_debuglog_stream<TCHAR, log_to_win32_debugger<TCHAR> > DebugLogger;

TCHAR 宏在多字节字符构建中包含 char,在 Unicode 构建中包含 wchar_t

按以下方式使用该类

DebugLogger(__FILE__, __LINE__, _T("main()")).get() << 
            _T("Hello debug log!") << std::endl;
DebugLogger(_T("main()")).get() << _T("Only a context message!\n");
DebugLogger().get() << _T("Without a context!\n");

这应该在调试器上产生以下输出

c:\projects\testlogger\main.cpp(20) : main() : Hello debuglog!
main() : Only a context message!
Without a context!

很简单,不是吗?也可以使用 STL 中的流修饰符。

DebugLogger("In hex") << std::hex << std::showbase << 12345 << std::endl;

这应该输出

In hex: 0x3039

为了摆脱打字之苦,我们定义了一些简单的宏。(宏?嗯,我知道宏很邪恶,但有时它们很有用。)

如果省略文件名和行号,我们使用前缀 RAW,如果使用上下文消息,我们使用前缀 CTX

#define RAWLOG() DebugLogger().get()
#define CTXRAWLOG(text) DebugLogger(text).get()
#define CTXLOG(text) DebugLogger(text, __FILE__, __LINE__).get()
#define LOG() DebugLogger(__FILE__, __LINE__).get()

现在,打字要容易得多

CTXLOG(_T("main()")) << _T("Hello debug log!") << std::endl;
CTXRAWLOG(_T("main()")) << _T("Only a context message!\n");
RAWLOG() << _T("Without a context!\n");

要在不使用 Visual Studio 的情况下捕获 OutputDebugString 的调试输出,请使用 Mark Russinovich(在 www.sysinternals.com,现归 Microsoft 所有)的免费工具 DebugView

记录到文件

记录到文件也很容易。只需为我们的调试日志流实现另一个函数对象。

template<class charT>
class log_to_file : public basic_log_function<charT>
{
public:
    result_type operator()(second_argument_type context,               
                           second_argument_type output)
    {    
        std::basic_ofstream<charT> fs(GetLogfilename(),std::ios_base::app);
        if (!fs)
            throw std::invalid_argument("Logging file not found!");
        else
            fs << context << output;
    }
private:
    const std::basic_string<charT> GetLogfilename()
    { 
        return std::basic_string<charT>(_T("c:\temp\debug.log"));
    }
};

typedef 
basic_debuglog_stream<TCHAR, log_to_file<TCHAR> > FileDebugLogger;

也许您想要一个更复杂的 GetLogFilename 实现,但是,这只是一个示例。

记录到 std::cerr

将输出定向到 std::cerr 甚至更简单(但因此,我们不需要那些类,但现在,我们可以以可互换的方式进行操作)。

template<class charT>
class log_to_cerr : public basic_log_function<charT>
{
public:
    result_type operator()(first_argument_type context,  
                            second_argument_type output)
    {
        std::cerr << context << output;
    }
};

typedef basic_debuglog_stream<TCHAR, log_to_cerr<TCHAR> > ErrDebugLogger;

有状态的函数对象

如您所知,您无法向函数对象传递更多信息。它们在流缓冲区的构造函数中实例化,并且无法访问它们。为了克服此限制,我建议使用 Monostate 模式,其中同一类的许多实例共享相同的状态。

template<class charT>
class MonoStateFunctor 
{
public:
    void operator()(const charT * const context, 
                    const charT * const message)
    {
        std::basic_ofstream<charT> fs(filename_.c_str(),
                                      std::ios_base::app);
        if (!fs)
            throw std::invalid_argument("cannot open filestream");
        else
            fs << context << message;        
    }
    
    void setFilename(const std::string &filename)
    {
        filename_ = filename;
    }
    const std::string getFilename() const
    {
        return filename_;
    }
private:
    static std::string filename_;        
};

typedef MonoStateFunctor<TCHAR> functor;
typedef basic_debuglog_stream<TCHAR, functor> logger;

使用此记录器

std::string functor::filename_ = "";

int main(int, char **)
{
    // The filename must be set once
    functor f;
    f.setFilename("c:\\temp\\test.log");

    logger(__FILE__, __LINE__, _T("main()")).get() << "This is a test!\n";
}

显然,您必须在多线程上下文中保护 filename_ 变量,例如使用互斥锁。

使用 MFC 类和您自己的类

如果您想将记录器与 MFC 中的类或您自己的类一起使用,您必须为它们定义流 operator<<,如以下代码片段所示,用于 CStringCOleDateTime

typedef std::basic_ostream<TCHAR> stream_type;

stream_type &operator<<(stream_type &log, const CString &text)
{
    log << text.operator LPCTSTR();
    return log;
}

stream_type &operator<<(stream_type &log, const COleDateTime &dateTime)
{
    log << dateTime.Format();
    return log;
}

int main(int, char **)
{
    CTXLOG(_T("main()")) << CString("MFC String: ") 
                         << COleDateTime::GetCurrentTime() 
                         << _T("\n");
}

编译器问题

我已使用 Visual Studio 2008、Visual Studio 6 和 GCC(Open Suse 10.3)测试了此代码。在 Visual Studio 6 上,我不得不将 std::basic_stringclear() 函数替换为 resize(0),并将调试级别设置为 3 而不是 4,以便在 STL 中编译时没有太多警告。对于我的 GCC 版本,我必须完全限定基类中的 typedef 或模板参数中的类型

例如:

virtual typename traits::int_type::int_type overflow (
                     typename traits::int_type int_type c);

参考文献

[1] Nicolai M. Josuttis,《C++ 标准库,教程和参考》

© . All rights reserved.