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






4.57/5 (13投票s)
一个易于使用的调试记录器,通过自定义流缓冲区实现。
引言
在 C++ 程序中输出调试日志消息有许多不同的方法。有些使用 printf
,另一些使用 std::cout
或 std::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()
函数将缓冲区刷新到输出目的地。为了使输出目的地可配置,我们使用一个函数对象并定义一个基类,该基类只保存函数调用运算符的参数和返回类型。为了使其通用,我们使用一个模板参数作为字符类型(char
或 wchar_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<<
,如以下代码片段所示,用于 CString
和 COleDateTime
。
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_string
的 clear()
函数替换为 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++ 标准库,教程和参考》