使用变参模板风格的调试打印






4.81/5 (10投票s)
使用 C++11 变参模板进行调试打印。
目录
引言
C++11(以前称为 C++0x)是 C++ 编程语言标准的最新版本。它于 2011 年 8 月 12 日获得 ISO 批准,取代了 C++03。C++11 为语言和标准库引入了许多新特性。值得注意的是,当 Visual C++ 11 发布时,可变参数模板支持等特性仍然缺失。微软最近发布了 Visual C++ 编译器 2012 年 11 月 CTP,它添加了以下 C++11 特性:统一初始化、初始化列表、可变参数模板、函数模板默认参数、委托构造函数、显式转换运算符和原始字符串。Stephan T. Lavavej 在 Channel 9 视频讲座中详细介绍了这些新增特性。可变参数模板使 C++ 程序员能够编写具有任意数量类型的模板类和函数。本文主要关注可变参数模板及其在实现 DebugPrint
中的应用。
可变参数打印
int printf ( const char * format, ... );
C 语言风格的可变参数 printf
(及其同类函数,如 fprintf
和 sprintf
)自 1970 年代 C 语言发明以来就存在,其本质上是不安全的。格式字符串中指定的类型可能与传递给 printf
的参数的实际数据类型不匹配。
int n = -1; // signed integer
printf("%u", n); // %u display 4294967295
C++11 中的可变参数模板允许模板编写者创建接受任意数量(不同)类型的模板;现在我们可以利用这个新特性来实现一个“安全的 printf
”函数。可变参数模板函数通常通过递归来实现。
// base function which stops the recursion.
void safe_printf(const char *s);
// variadic recursive function
template<typename T, typename... Args>
void safe_printf(const char *s, T value, Args... args);
我们知道 Args
是可变的,这由 typename... Args
和 Args... args
指示。Args
可以是零个或多个类型。当它为空时,就会调用基础的 safe_printf
。请注意,基础 safe_printf
不是可变的!让我们看看函数体。
void safe_printf(const char *s)
{
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
throw std::runtime_error("invalid format string: missing arguments");
}
}
std::cout << *s++;
}
}
template<typename T, typename... Args>
void safe_printf(const char *s, T value, Args... args)
{
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
std::cout << value;
safe_printf(s + 1, args...); // call even when *s == 0 to detect extra arguments
return;
}
}
std::cout << *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
为了说得更清楚一些,这个函数实际上并不是真正意义上的递归。传统的递归函数会直接或间接调用自身。当可变参数模板函数调用自身时,它实际上是在调用一个同名但参数数量不同的函数。这个 safe_printf
是从 维基百科页面复制的。safe_printf
调用 cout
来完成工作,因此只要程序员为特定类型重载了 <<
运算符,它就可以显示任何任意类型。当参数不足时,它会抛出 runtime_error
;当参数过多时,它也会抛出 logic_error
。调用方式如下。
safe_printf("Product:%, Qty:%, Price is $%\n ", "Shampoo", 1200, 2.65);
// prints "Product:Shampoo, Qty:1200, Price is $2.65"
注意:我们不需要在格式字符串中指定类型:我们只使用 %
作为占位符。要字面打印 %
,请写 %%
进行转义。C++ 程序员可以使用 sizeof...(args)
在编译时找出参数的数量。调用 sizeof(args)...
会展开为对每个参数调用 sizeof
。用户可以对任何函数执行此操作,而不仅仅是 sizeof
。
注意:本文主要介绍函数编写,而非类。要了解更多关于编写可变参数模板类的信息,请参考 相关部分中的视频链接。
可变参数调试打印
在可变参数调试打印出现之前,程序员通常会使用 sprintf
来格式化文本,然后再使用 OutputDebugString
显示。
char buf[50];
sprintf(buf, "Product:%s, Qty:%d, Price is $%f\n ", "Shampoo", 1200, 2.65);
OutputDebugStringA(buf);
sprintf 的一个问题是给定的缓冲区可能不够大以容纳结果文本。因此,GNU 和 BSD 推出了 asprintf
和 vasprintf
作为 C 或 POSIX 的扩展。asprintf
和 vasprintf
函数是 sprintf
和 vsprintf
的模拟,不同之处在于它们会分配一个足够大的字符串来容纳输出(包括终止的空字节),并通过第一个参数返回指向该字符串的指针。当不再需要时,应将此指针传递给 free
来释放分配的存储。注意:这些函数仅在 Linux 和 BSD 上可用。
char *pbuf = NULL;
asprintf(&pbuf, "Product:%s, Qty:%d, Price is $%f\n ", "Shampoo", 1200, 2.65);
/* use here */
free(pbuf);
在本节中,我们将 safe_printf
转换为一个版本,该版本使用 Windows API OutputDebugString
打印调试信息,而不是通过 cout
输出到控制台。
#include <sstream>
#include <Windows.h>
void xsprintf(std::string& result, const char *s)
{
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
throw std::runtime_error("invalid format string: missing arguments");
}
}
result += *s++;
}
}
template<typename T, typename... Args>
void xsprintf(std::string& result, const char *s, T value, Args... args)
{
while (*s) {
if (*s == '%') {
if (*(s + 1) == '%') {
++s;
}
else {
std::stringstream stream;
stream << value;
result += stream.str();
xsprintf(result, s + 1, args...); // call even when *s == 0 to detect extra arguments
return;
}
}
result += *s++;
}
throw std::logic_error("extra arguments provided to printf");
}
template<typename... Args>
void DebugPrint(const char *s, Args... args)
{
#ifdef _DEBUG
std::string result = "";
xsprintf(result, s, args...);
OutputDebugStringA(result.c_str());
#endif
}
递归方法的问题在于,每次函数调用都必须携带状态信息(如果有的话)。DebugPrint
有一个名为 result
的状态信息;我们不能像使用 cout
那样逐块打印信息,因为 OutputDebugString
会在 Sysinternals DebugView 中(但在 Visual Studio 调试器中不会)将它们分割成新行。而且我们不希望其调用者了解这个实现细节。因此 DebugPrint
不能是递归的,而是调用一个递归函数 xsprintf
来完成工作。xsprintf
利用 stringstream
来完成其工作。stringstream
可以用来将 POD 类型转换为 string
(使用 <<
)反之亦然(使用 >>
)。
调用 DebugPrint
的方式与上一个示例类似。
DebugPrint("Product:%, Qty:%, Price is $%\n ",
"Shampoo", 1200, 2.65); // prints "Product:Shampoo, Qty:1200, Price is $2.65"
可变参数调试打印(.NET 风格)
如果我们更喜欢 .NET 的 string
占位符说明符,例如第一个参数的 {0}
,第二个参数的 {1}
,依此类推呢?我也完成了该版本。此外,我还使其支持 Unicode。但此版本在缺少或有多余参数时不会抛出任何异常:程序员会在输出中注意到这种差异。这是为了遵循原始的非可变参数 DebugPrint
(在下一节介绍)的行为。
#include <string>
#include <sstream>
#include <Windows.h>
std::wstring Anchor( int i )
{
std::wstringstream stream;
stream << i;
std::wstring str = L"{";
str += stream.str() + L"}";
return str;
}
std::wstring Replace( std::wstring fmtstr, size_t index, const std::wstring& s )
{
size_t pos = 0;
std::wstring anchor = Anchor( index );
while( std::wstring::npos != pos )
{
pos = fmtstr.find( anchor, pos );
if( std::wstring::npos != pos )
{
fmtstr.erase( pos, anchor.size() );
fmtstr.insert( pos, s );
pos += s.size();
}
}
return fmtstr;
}
std::wstring Format( std::wstring fmt, size_t index )
{
return fmt;
}
template<typename T, typename... Args>
std::wstring Format( std::wstring fmt, size_t index, T& t, Args&... args )
{
std::wstringstream stream;
stream << t;
std::wstring result = Replace( fmt, index, stream.str() );
++index;
std::wstring str = Format( result, index, args... );
return str;
}
template<typename... Args>
void DebugPrint(const wchar_t *s, Args... args)
{
#ifdef _DEBUG
std::wstring str = Format(std::wstring(s), 0, args...);
OutputDebugStringW(str.c_str());
#endif
}
DebugPrint
调用递归的 Format
函数来完成工作。Anchor
函数根据索引返回 "{x}"
string
,而 Replace
将使用此锚点来查找并用参数替换它们。.NET 自定义格式说明符(如“{0:f2}”)不受支持。换句话说,用户最好使用之前的 %
版本,因为 "{x}"
除了减慢查找和替换的处理速度之外,没有任何其他用途。
非可变参数调试打印(.NET 风格)
void Print( const wchar_t* fmt, Box D1, Box D2 );
在此之前,有一个编写于 2007 年的 DebugPrint
类版本,该版本大量使用了模板模式(该模式与 C++ 模板无关)。该类重载了 Print
函数,其参数数量从 0 到 10 个 Box
参数不等,以提供可变参数的假象,并且不再积极维护。如果用户需要打印 11 个变量,那就无能为力了。Box
通过为每种 POD 类型提供重载的构造函数来工作,并将 POD 变量转换为 string
。
print(L"{0}{1}", 1, 1.0f);
print(L"{0}{1}", 1.0f, 1.0f);
print(L"{0}{1}", 1, 1);
print(L"{0}{1}", 1.0f, 1);
假设我们使用可变参数模板版本编译上面的代码一次,然后使用重载版本编译另一次。可变参数模板版本将实例化 4 个不同的版本(见下文),这可能会导致代码膨胀(至少理论上如此),如果使用了许多不同的组合。然而,总的来说,可变参数模板代码被发现具有更小的二进制文件大小和更快的编译时间。
void print(wstring, int, float);
void print(wstring, float, float);
void print(wstring, int, int);
void print(wstring, float, int);
对于重载版本,会调用相同的重载函数(见下文) 4 次。另一个优点是,如果无法使用最新的 C++11 编译器选项,则重载版本可以与旧版编译器一起使用。
void print(wstring, box, box);
源代码
要使用 Visual C++ 编译器 2012 年 11 月 CTP 提供的新 C++11 特性,用户必须在项目配置的“常规”下手动选择编译器。请注意:‘Microsoft Visual C++ 编译器 2012 年 11 月 CTP’仅供测试使用。
结论
我们已经看到,可变参数模板函数通常是递归实现的(尽管如前所述,并非真正的递归)。文中提到了重载函数与可变参数模板的区别。强烈建议读者下载并查看源代码。
相关链接
如上所述,如果读者有兴趣了解更多关于可变参数模板的信息,特别是关于可变参数模板类以及 std::tuple
是如何实现的,我强烈建议观看这个信息丰富的视频。
我将谦虚地推荐任何有兴趣使用 C++11 可变参数模板库来读写文件的人访问以下链接。
- 作者的 文本和二进制文件 API 的统一
历史
- 2012-12-30:包含了一个简短的
asprintf
段落 - 2012-12-23:首次发布