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

以流式方式进行调试跟踪

2006年8月13日

CPOL

6分钟阅读

viewsIcon

90092

downloadIcon

452

使用 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::ostreamwostream 不合适,因为它们是预定义的字符类型。

我们需要另一个类型定义。在 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[] 缓冲区,保留一个字符作为 **守卫**,用作字符串终止符。

此实现的关键组件是 setbufoverflowsync 虚函数重载。

setbuf 重载只需调用基类的 setp 函数,告诉它使用从第一个到倒数第二个字符的提供的缓冲区(从而将最后一个字符留空)。

当遇到 std::flushstd::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(.) 宏。(这正是我在本篇文章的示例代码段中没有使用的)。

© . All rights reserved.