每周技巧 #3:字符串拼接:`operator+` 与 `StrCat()`

本节阅读量:

本文翻译自 Abseil 官网的 Tip of the Week #3: String Concatenation and operator+ vs. StrCat()

原文最初作为 TotW #3 发布于 2012 年 5 月 11 日。

更新于 2022 年 11 月 16 日。

用户常常会惊讶地听到评审者说:“不要用字符串拼接运算符,它没那么高效。”string::operator+ 怎么会低效呢?这种东西难道也容易写错吗?

事实证明,这种低效并不是非黑即白的。实践中,下面两段代码的执行时间非常接近:

1
2
3
4
5
6
7
std::string foo = LongString1();
std::string bar = LongString2();
std::string foobar = foo + bar;

std::string foo = LongString1();
std::string bar = LongString2();
std::string foobar = absl::StrCat(foo, bar);

但是,下面这两段代码就不是这样了:

1
2
3
4
5
6
7
8
9
std::string foo = LongString1();
std::string bar = LongString2();
std::string baz = LongString3();
std::string foobarbaz = foo + bar + baz;

std::string foo = LongString1();
std::string bar = LongString2();
std::string baz = LongString3();
std::string foobarbaz = absl::StrCat(foo, bar, baz);

这两种情况为什么不同,可以从拆开 foo + bar + baz 这个表达式开始理解。C++ 中没有三参数运算符重载,因此这个操作必然会调用两次 string::operator+。而在这两次调用之间,它会构造并保存一个临时字符串。所以 std::string foobarbaz = foo + bar + baz 实际上等价于:

1
2
std::string temp = foo + bar;
std::string foobarbaz = std::move(temp) + baz;

特别要注意,foobar 的内容必须先复制到一个临时位置,然后才会放入 foobarbaz。(关于 std::move 的更多内容,见 技巧 #77。)

C++11 至少允许第二次拼接不创建新的字符串对象:std::move(temp) + baz 等价于 std::move(temp.append(baz))。不过,临时对象最初分配的缓冲区可能不够容纳最终字符串,此时就需要重新分配内存,并再次复制。因此,在最坏情况下,由 n 次字符串拼接组成的链式表达式需要 O(n) 次重新分配。

更好的做法是使用 absl::StrCat()。这是 absl/strings/str_cat.h 中一个很好用的辅助函数:它会计算所需字符串长度,预留对应大小,然后把所有输入数据拼接到输出中。这是经过良好优化的 O(n) 做法。类似地,对于下面这样的代码:

1
foobar += foo + bar + baz;

请使用 absl::StrAppend(),它会执行类似的优化:

1
absl::StrAppend(&foobar, foo, bar, baz);

此外,absl::StrCat()absl::StrAppend() 不只适用于字符串类型。你可以使用 absl::StrCat/absl::StrAppend 转换 int32_tuint32_tint64_tuint64_tfloatdoubleconst char*string_view,例如:

1
std::string foo = absl::StrCat("The year is ", year);

更多信息见 absl::StrCat() and absl::StrAppend() for String Concatenation

每周技巧 #1:`string_view`

上一节

每周技巧 #5:消失术

下一节


本节目录