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

C++ StringBuilder 类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.22/5 (5投票s)

2007年9月14日

4分钟阅读

viewsIcon

77296

downloadIcon

621

介绍用 C++ 编写的 StringBuilder 类的文章。

Sample Image - maximum width is 600 pixels

引言

本文介绍了一个 C++ 类,该类能够将不同类型参数的组合方便地构建成一个字符串。目前,C++ 标准库中没有与 .NET 的 System.Text.StringBuilder 相对应的类。然而,将不同类型参数的值嵌入到字符串中是非常常见的需求,例如在向文件记录信息时,或是在向用户显示消息时。

本文介绍的 StringBuilder 类正是为了满足这一需求而设计的。它利用了 C++ 本身的特性,并为用户提供了一种优雅且简洁的方式,可以基于不同类型的参数创建字符串。

背景

最近,在实现一个线程安全的循环缓冲区时,我遇到了一个方便的机制的需求,以便将缓冲区的状态记录到文件中。这意味着需要能够创建一个字符串,该字符串由不同类型参数的值组成(这些参数共同决定了缓冲区的状态),然后将其传递给一个日志函数,该函数会将该字符串写入文件。实现此功能的一种方法是使用 sprintf

int index = 0;
... // cyclic buffer undergoes changes

char string_buf[100];
sprintf(string_buf, "%Current index is: %d.", index);
string message(string_buf);
log(message); // log() is implemented elsewhere

这种方法的缺点是它非常繁琐。我们创建一个包含单个参数值的格式化字符串的需求,需要转化为四行 C++ 代码。此外,我们需要为 sprintf 分配一个缓冲区,并且在大多数情况下,该缓冲区的确切大小无法提前确定。因此,我们面临缓冲区溢出的风险。

这时,我们可能会想到包装 fprintf 函数。fprintf 的(简化后的)签名如下:

int fprintf(FILE *_file, const char *_format, ...);

fprintf 的基本思想是,它使用第二个参数(_format),类型为 const char *,来读取后面的参数。... 参数使得 fprintf 成为一个可变参数函数(即可以接受可变数量参数的函数)。因此,如果我们暴露一个具有以下签名的日志函数……

int log(const char *format, ...);

……那么我们可以将接收到的参数传递给 fprintf,从而用以下代码达到我们的目标:

int log(const char *format, ...) 
{
    va_list argumentsList;
    va_start(argumentsList, format);
    
    FILE *pLogFile = fopen("log.txt","w");
    int retVal = fprintf(pLogFile, format, argumentsList);
    fclose(pLogFile);
    
    va_end(argumentsList);
    
    return retVal;
};

不幸的是,尽管这段代码可以编译,但它很可能无法达到我们的预期。这是因为从编译器的角度来看,我们对 printf 的调用被视为一个只涉及两个参数的过程调用,这显然违背了我们支持任意数量参数的初衷。

回到原点……现在看来我们有两个可行的选项:

  • 重新实现 printf 系列函数所使用的机制(该机制包括一个格式化字符串以及之后任意长度的参数列表)。
  • 做一些更酷的事情,利用转换运算符、运算符重载和隐式构造函数。这种创新的方法还应该利用 C++ 是静态类型这一事实,以确保生成的字符串问题可以在编译器而不是在运行时被捕获(printf 系列无法提供此保证)。

理所当然,我选择了第二种方法。

Using the Code

正如您可能已经猜到的,我编写的代码使用了我提到的所有“宝藏”。从根本上说,它分为两个类:StringElementStringBuilderStringElement 对象可以使用各种类型进行构造。StringElement 的构造函数都不是显式的,这意味着它可以从一个长(且可扩展)的类型列表中隐式构造。

StringBuilderoperator<< 利用了这一点。

StringBuilder &operator<<(StringElement se) 
{
    Append(se); return *this;
};

它接受一个 StringElement 类型的参数,并使用以下代码将其附加到其 _value 成员(类型为 std::string)上:

void Append(StringElement element) 
{
    _value.append(element);
};

为了让这段代码编译通过,StringElement 必须能够转换为 std::string,事实上它就是这样做的。由于 operator<< 的返回值是 StringBuiler &(即 StringBuilder 的引用),并且由于 operator<< 是左结合的,所以它支持级联调用。最后但同样重要的是,StringBuilder 可以转换为 char *std::string,这使得以下代码可以编译:

printf(StringBuilder() << "x=" << x << " and y=" << y);

剩下的唯一一点就是向用户隐藏 printf 调用过程中创建 StringBuilder 对象的事实。这是通过一个简单的宏来实现的。最终结果如下:

printf(SB << "x=" << x << " and y=" << y);

如上所述,与 printf 使用的机制相比,这里采取的方法的一个主要优势是它允许我们在编译时捕获格式错误的字符串。这是因为我们传入的参数被编译成 StringElement 对象(使用 StringElement 上定义的隐式构造函数),从那时起一切都应该顺利进行,正如 StringElement 的实现所清楚表明的那样。考虑到我由于格式字符串中的拼写错误而经历的崩溃次数,我认为这是我所采取的方法的一个显著优势。

历史

  • 2007 年 9 月 14 日 -- 发布原始版本
© . All rights reserved.