前言

其实实现个StrAppend和StrCat早就有所考虑了,不过近来的编码没有这方面需求(其实有的,不过不是热点问题),所以也就作罢。
这方面其实早就听闻abslstring util库,看了一下文档,有类似的设施:StrAppend()StrCat()
不过为了两个函数,而引入个第三方库,未免有点头大。因此我还是打算实现自己的版本。
只是出乎我意料的是,我的实现似乎还优于absl的。

正确的StrAppend姿势

对于一个cpp新手来说,可能会通过下面这种方式进行append:

1
2
string s = ...;
s += s1 + s2 + s3 + ...;

稍微对运算符重载了解的想必都知道每个运算符会生成一个临时对象,因此上述表达式等效于:
1
2
s += ((s1 + s2) + s3) + ...;
^ ^

因此一些教材或是论坛回答会建议用多语句的append替代:
1
2
3
4
s += s1;
s += s2;
s += s3;
s += ...;

不否认,这样避免了临时对象的生成,但是仍不是最优的写法。
我们可以预分配目标字符串的内存空间,从而避免多次append带来的多次内存重分配。
1
s.reserve(s.size() + s1.size() + s2.size() + s3.size() + ...);

然而,随之而来的新问题是这样写破坏了可读性且繁琐。

等效替代

仔细一想,要添加哪些字符串,字符串个数都是编译时已知的信息,因此我们可以考虑通过模板元编程(TMP)中一个技巧:递归模板实例化(recurring template instantiation)在编译时进行展开,最终等效于上述写法。
我们可以在递归的出口函数中进行预分配,以及最后一个字符串实参的拷贝,然后递归返回时,不断将前面的空间填充即可。
因此需要准备一个sizeindex实参,前者记录至今所有字符串的总长度,而index记录填充的开始位置,等递归返回时就可以知道拷贝空间的尾部位置。
为了兼容std::stringstd::string_view(c++17)char const*(C-style),重载三个版本。

理解了这些思路,下面的代码是十分简单的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include <string>
#include <string.h>
#if __cplusplus >= 201703L
#define CXX_STANDARD_17
#endif

#ifdef CXX_STANDARD_17
#include <string_view>
#endif

namespace util {
namespace detail {
/*
* 利用递归模板实例化在编译时展开模板,从而避免了运行时循环开销
* 由于递归的特性,可以在最后的出口函数进行预分配,
* 并从尾部开始拷贝最后一个字符串
*
* 为兼容C-style,std::string,std::string_view(c++17),重载了三个版本
*/

#define STRAPPEND_STL_STYLE \
do { \
size += head.size(); \
StrAppend_impl(str, size, index, strs...); \
assert(index - head.size() >= 0); \
index -= head.size(); \
memcpy(&str[index], head.data(), head.size()); \
} while (0)

/*
* 注意,这里必须前向声明,不然递归实例化时,实例化点可能看不到在下面定义的其他StrAppend_impl(),因此重载决议并不会考虑它们
* 因此诸如
* `auto s = StrCat("A", std::string("A"), std::string_view("A"),
* std::string_view("A"))`就会报错
*/
template <typename... Args>
void StrAppend_impl(std::string &str, size_t &size, size_t &index,
std::string const &head, Args &&... strs);

template <typename... Args>
void StrAppend_impl(std::string &str, size_t &size, size_t &index,
char const *head, Args &&... strs);

template <typename... Args>
void StrAppend_impl(std::string &str, size_t &size, size_t &index,
std::string_view const &head, Args &&... strs);

template <typename... Args>
void StrAppend_impl(std::string &str, size_t &size, size_t &index,
std::string const &head, Args &&... strs)
{
STRAPPEND_STL_STYLE;
}

template <typename... Args>
void StrAppend_impl(std::string &str, size_t &size, size_t &index,
char const *head, Args &&... strs)
{
auto len = strlen(head);
size += len;
StrAppend_impl(str, size, index, strs...);
assert(index - len >= 0);
index -= len;
memcpy(&str[index], head, len);
}

#ifdef CXX_STANDARD_17
template <typename... Args>
void StrAppend_impl(std::string &str, size_t &size, size_t &index,
std::string_view const &head, Args &&... strs)
{
STRAPPEND_STL_STYLE;
}
#endif

#define STRAPPEND_STL_STYLE_EXIT \
do { \
size += head.size(); \
str.resize(size); \
index = size - head.size(); \
memcpy(&str[index], head.data(), head.size()); \
} while (0)


template <>
inline void StrAppend_impl<>(std::string &str, size_t &size, size_t &index,
std::string const &head)
{
STRAPPEND_STL_STYLE_EXIT;
}

template <>
inline void StrAppend_impl<>(std::string &str, size_t &size, size_t &index,
char const *head)
{
auto len = strlen(head);
size += len;
str.resize(size);
index = size - len;
memcpy(&str[index], head, len);
}

#ifdef CXX_STANDARD_17
template <>
inline void StrAppend_impl<>(std::string &str, size_t &size, size_t &index,
std::string_view const &head)
{
STRAPPEND_STL_STYLE_EXIT;
}
#endif

} // namespace detail

/**
* \brief Append strings efficiently than str.append() with loop or sequence
*
* The function will preallocate the space and append them to the \p str,
* and expand them in compile time, so there is no runtime overhead.
* \note
* The performance is better than the absl::StrAppend()
*/
template <typename... Args>
void StrAppend(std::string &str, Args &&... strs)
{
size_t size = 0;
size_t index = 0;
detail::StrAppend_impl(str, size, index, strs...);
}

/**
* \brief Concatenate the strings to a single string efficiently
* \note
* In fact, this a wrapper of the StrAppend(empty-string, strs...)
*/
template <typename... Args>
std::string StrCat(Args &&... strs)
{
std::string ret;
StrAppend(ret, strs...);
return ret;
}

} // namespace util

benchmark


进行基准测试主要是和absl对比,至于std::string是可预见的差距悬殊。

由图分析不难看出,我的实现比absl还要高效,稍微有点出乎意料但或许是情理之中(自然,是Release模式下测的)。

戳-> 测试代码