以流式方式进行调试跟踪






4.74/5 (18投票s)
使用 std::ostream 在调试窗口中进行跟踪
引言
本文是“第二版”,取代了旧版本,回应了前者的某些请求,并完善了某些设计方面。
最初的想法是在编写一个小型程序进行某些数学计算时出现的,该程序未能按预期工作。在某些执行点需要跟踪一些值,通常是为了跟踪执行流程。
在控制台应用程序中,这通常是通过 std::cout
完成的,但如果您想保持控制台免受调试干扰,或者您使用的是没有控制台的 Windows 应用程序,这可能不是一个好的选择。
老式的 C 运行时 <crtdbg.h>
头文件提供了一些名为 _RPTx
的类似 printf
的宏。但是,如果我们想要一些能够支持通用 C++ 风格接口、可能适用于所有类型的东西,那么就缺少了某些东西。
Win32 的 OutputDebugString
可以提供帮助,但它只是一个 LPCTSTR
输出函数。所以,想法是使用该 API 编写一个类似 STL 的流用于输出。
目录
深入了解流
最直观的解决方案是创建一个类,该类具有一个重载的 operator<<
,它在将输入数据转换为字符串后调用 Win32 API。
事实上,这样做将导致重新发明大部分 STL 格式化实用程序。
STL 流实际上都是对通用 std::basic_ostream
基类的重新解释,为它提供了一个方便的 std::basic_streambuff
类的重载。
流通过选定的 **locale** 处理数据格式化,提供实用程序,而 **streambuff** 为流提供写入空间。流还负责向 **media** 输出(在本例中是调试器;更一般地,是标准输出或打开的文件)。
因此,处理此任务的正确方法是提供 basic_streambuff
的重载,将提供的缓冲区内容刷新到调试输出。
待处理的事项
如果我们想在纯 Win32 上下文中使用这些函数,我们必须准备好处理 TCHAR
。出于这个原因,std::ostream
和 wostream
不合适,因为它们是预定义的字符类型。
我们需要另一个类型定义。在 GE_::dbg
命名空间中,我放置了一个 ostream_t
类型定义,它别名为 basic_ostream<TCHAR>
。
然后使用一个内部的 report_h
命名空间来包含实现细节。在其中,globals
类将 ostream_t stream
对象和 streambuff
对象声明为成员,其中 streambuff
是一个派生自 std::basic_streambuf<TCHAR>
的类。
为了提供跟踪缩进,还存在一个 depth
成员。
无论 streambuff
如何实现,globals
构造函数都会初始化流,传递 streambuff 对象地址。此外,内联的 global
函数提供对 globals
的惰性静态实例的访问。
在内部命名空间之外,GE_::dbg::out()
返回对隐藏的静态流的引用,使其全局可用。
流缓冲区实现
streambuff
类派生自 std::basic_sreambuff<TCHAR>
,其构造函数调用继承的 pubsetbuff
,设置一个提供的 TCHAR[]
缓冲区,保留一个字符作为 **守卫**,用作字符串终止符。
此实现的关键组件是 setbuf
、overflow
和 sync
虚函数重载。
setbuf
重载只需调用基类的 setp
函数,告诉它使用从第一个到倒数第二个字符的提供的缓冲区(从而将最后一个字符留空)。
当遇到 std::flush
或 std::endl
操纵符时,流会调用 sync
函数刷新缓冲区。它只需获取当前运行的 pbase()
和 pptr
()(流输出的开始和最后一个输出字符),计算字符串大小,强制将最后一个输出字符设置为 '\0'
,然后调用 OutputDebugString
,从而导致缓冲区内容被打印出来。之后,它调用 pbump(-sz)
来回滚缓冲区,允许流覆盖它。
当流发送到输出的字符多于缓冲区可容纳的字符时,会调用 overflow
函数。它只需调用 sync
,从而刷新缓冲区,将“待处理”字符放入刚清空的缓冲区中,并增加写入位置。
简单示例
上面描述的代码是此代码段所需的所有内容
int n = 5;
double d = 6.75;
dbg::out() << "n = " << n << "; d = " << d << std::endl;
用于跟踪类似下面这样的行
n = 5; d = 6.75
RAII 缩进
dbg::trace
类是一个 RAII 类(析构函数“撤销”构造函数对资源的所作所为的类),它有助于跟踪函数调用的进入/退出。
它接受 **name** 和 **address** 参数,并在构造/析构时增加/减少 globals.depth
成员。
其构造函数以 >
符号开头,并在 depth
宽度字段中写入传入的 name[address]。同样,析构函数写入 <name[address],具有相同的缩进。
通用的 operator<<
将参数输出到流,并在前面加上缩进的“.”。此“效果”也可以通过正常“<<
”链中的 dbg::dpeth
操纵符获得。
因此,此函数
void function(int n)
{
dbg::trace trc("function", function);
trc << "hallo with n = " << n << std::endl;
if(n) function(n-1);
}
如果调用 n = 3,将跟踪为
>function[xxxxxxxx]
.hallo with n = 3
>function[xxxxxxxx]
.hallo with n = 2
>function[xxxxxxxx]
.hallo with n = 1
>function[xxxxxxxx]
.hallo with n = 0
<function[xxxxxxxx]
<function[xxxxxxxx]
<function[xxxxxxxx]
<function[xxxxxxxx]
RAII 对象跟踪
dbg::track
类与 dbg::trace
类似,但它使用 "C:"
和 "D:"
字符串而不是“<”和“>”符号,并且不增加/减少深度(但在与它一致的情况下写入)。
它可以用于跟踪对象的创建/销毁,例如在此代码段中
class myobject
{
private:
dbg::track trk;
public:
myobject() :trk("myobject", &trk) {}
};
并使用以下代码
int main()
{
dbg::trace trc("main", 0);
trc << "creating two objects on stack" << std::endl;
myobject m1, m2;
trc << "creating a static object" << std::endl;
static myobject m3;
trc << "Bye" << std::endl;
}
将跟踪为
>main[00000000]
.creating two object on stack
C:myobject[xxxxxxxx]
C:myobject[yyyyyyyy]
.creating a static object
C:myobject[zzzzzzzz]
.Bye
D:myobject[yyyyyyyy]
D:myobject[xxxxxxxx]
<main[00000000]
D:myobject[zzzzzzzz]
(请注意静态对象在 main
返回后是如何销毁的)
其他事项
插入跟踪代码的一个问题是如何在发布版本中删除它(以避免跟踪字符串字面量和可能冗长的函数调用)。
在上一篇文章中,我提出了使用“假流”的建议,该流的操作实现为“什么都不做”,让编译器优化过程删除假调用。
在这里,我使用了一种更传统的方法。如果定义了 _DEBUG
符号,DBG
宏就定义为它本身,否则就定义为空(“nothing”)。
可以像此代码段一样放置调用
void myfunc()
{
DBG(dbg::trace trc("myfunc", myfunc);)
...
DBG(trc << "some code" << a_variable << std::endl;)
...
}
或者在被跟踪对象中插入一些虚拟变量,例如
class myclass
{
DBG(dbg::trackedobj<myclass> trk;)
public:
...
};
结论
我希望这个新版本能比其前身更灵活、更易用。旧文章中的一些“功能”(如 stackval
操纵符)在这里不存在,主要是因为在使用 RAII 包装器时不需要它们。
无论如何,我已经将旧文章文本和嵌入的代码放在这个子页面中,供希望了解历史的人们参考。
编译器兼容性
这里提供的代码在 C++ 8.0 (VSC++ express) 中编译。代码本身对编译器并不特别敏感,但编译器提供的不同版本的 STL 可能会有所不同。
一个特殊情况是 UNICODE 的使用:当 TCHAR
定义为 char
(非 unicode)时,std::ostream
会将 wchar_t*
类型视为 void*
(打印地址的十六进制表示)。
当定义了 UNCODE 时,C++8 STL 会将 char*
和 wcahr_t*
都视为 std::wostream
中的字符串。这允许您在 unicode 环境中也将调试字符串字面量写为 C++ 字符串(const char[]
)。
不幸的是,这不能保证始终有效:旧的 C++ 编译器提供的 STL 实现可能不会将 char*
表示为 std::wostream
中的“字符串字面量”,当 TCHAR 定义为 wchar_t
时。教训:除非您确切知道您正在使用哪个 STL 版本提供商,否则请始终使用 _T(.)
宏。(这正是我在本篇文章的示例代码段中没有使用的)。