在开发过程中,调试是不可或缺的一部分。对于复杂的程序,跟踪程序的执行流程和变量的值是非常有帮助的。本文将介绍如何在C++中实现一个类似于标准模板库(STL)风格的调试输出流,以及如何使用RAII模式进行函数调用和对象创建的跟踪。
在编写一个用于特定数学计算的小程序时,发现程序的执行结果与预期不符。为了追踪程序的执行流程和变量的值,需要在特定的执行点输出一些信息。在控制台应用程序中,通常使用std::cout
来实现这一功能。但是,如果希望保持控制台的整洁,或者在没有控制台的Windows应用程序中,这种方法可能不太适用。
解决方案
在Windows平台上,可以使用OutputDebugString
函数来实现这一功能。但是,这个函数只能输出一个字符串,不能支持C++风格的接口。因此,目标是编写一个类似于STL流的类,使用OutputDebugString
函数来输出数据。
实现细节
为了实现这个目标,需要创建一个类,该类重载了operator<<
运算符,以便在调用Win32 API之前将输入数据转换为字符串。通过这种方式,可以避免重新实现STL的大部分格式化工具。
在STL中,所有的流都是基于std::basic_ostream
基类的重新解释,为其提供了一个方便的std::basic_streambuf
类的覆盖。流负责数据格式化,并通过选定的locale
提供工具,而streambuf
则提供了流写入的空间。流还负责将输出定向到媒体(在本例中是调试器;更一般地,是标准输出或打开的文件)。
为了正确处理这个任务,需要提供一个basic_streambuf
的覆盖,将提供缓冲区的内容刷新到调试输出中。
如果希望在纯Win32环境中使用这样的函数,必须准备好使用TCHAR
。因此,std::ostream
和std::wostream
不适用,因为它们是预定义的字符类型。
需要另一个类型定义。在GE_::dbg
命名空间中,放置了一个ostream_t
类型定义,它是basic_ostream
的别名。
一个内部的report_h
命名空间用于包含实现细节。在其中,类globals
声明为成员的ostream_t
流对象和streambuf
对象,其中streambuf
是从std::basic_streambuf
派生的类。
为了提供跟踪缩进,还有一个depth
成员。不管streambuf
的实现如何,globals
构造函数初始化流,传递streambuf
对象的地址。此外,内联的global
函数提供了对globals
的懒静态实例的访问。
在内部命名空间之外,GE_::dbg::out()
返回对静态隐藏流的引用,使其全局可用。
streambuf
类从std::basic_streambuf
派生,其构造函数调用继承的pubsetbuf
,设置一个提供的TCHAR[]
缓冲区,保留一个字符作为guardian
,用作字符串终结符。
这个实现的关键组件是setbuf
、overflow
和sync
虚拟函数的覆盖。
setbuf
覆盖简单地调用基setp
函数,告诉它使用从第一个到倒数第二个字符的提供的缓冲区(因此总是保留最后一个字符为null)。
sync
函数在流需要刷新缓冲区时被调用。当遇到std::flush
或std::endl
操作符时会发生这种情况。它只是获取当前运行的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
dbg::trace
类是一个RAII类(一个其析构函数“撤销”了构造函数所做的资源的类),它有助于跟踪函数调用的进入/退出。
它接受name
和address
参数,并在构造/析构时增加/减少globals.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
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
返回后被销毁)
其他事项
插入跟踪代码的一个问题是使其在发布版本中消失(以避免跟踪字符串字面量和可能的冗长函数调用)。
在之前的文章中,提出了使用“假流”的方法,其操作实现为“什么都不做”,让编译器优化过程移除假调用。在这里,使用了一种更传统的方法。DBG
宏定义为如果定义了_DEBUG
符号,则为其参数;如果没有定义,则为“无”。
可以像这个代码片段一样放置调用:
void myfunc() {
DBG(dbg::trace trc("myfunc", myfunc);)
...
DBG(trc << "some code" << a_variable << std::endl;)
...
}
或者在跟踪对象中插入一些虚拟变量,如:
class myclass {
DBG(dbg::trackedobj trk;)
public:
...
};
希望这个新版本比其前身更加灵活和可用。一些旧文章的“功能”(如stackval
操作符)在这里没有出现,主要是因为如果使用RAII包装器,它们不是必需的。
无论如何,已经将旧文章的文本和嵌入代码放在这个子页面上,供任何想查看历史的人使用。
这里介绍的代码是在C++8.0(VSC++ express)中编译的。代码本身对编译器不太敏感,但是编译器提供的STL的不同版本可能会有所不同。
一个特殊情况是使用UNICODE:当TCHAR
定义为char
(无unicode)时,wchar_t*
类型由std::ostream
作为void*
处理(打印地址的十六进制表示)。