高效的StrAppend和StrCat实现
前言
其实实现个StrAppend和StrCat早就有所考虑了,不过近来的编码没有这方面需求(其实有的,不过不是热点问题),所以也就作罢。
这方面其实早就听闻absl有string util
库,看了一下文档,有类似的设施:StrAppend()
和StrCat()
。
不过为了两个函数,而引入个第三方库,未免有点头大。因此我还是打算实现自己的版本。
只是出乎我意料的是,我的实现似乎还优于absl的。
正确的StrAppend姿势
对于一个cpp新手来说,可能会通过下面这种方式进行append:1
2string s = ...;
s += s1 + s2 + s3 + ...;
稍微对运算符重载了解的想必都知道每个运算符会生成一个临时对象,因此上述表达式等效于:1
2s += ((s1 + s2) + s3) + ...;
^ ^
因此一些教材或是论坛回答会建议用多语句的append替代:1
2
3
4s += s1;
s += s2;
s += s3;
s += ...;
不否认,这样避免了临时对象的生成,但是仍不是最优的写法。
我们可以预分配目标字符串的内存空间,从而避免多次append带来的多次内存重分配。1
s.reserve(s.size() + s1.size() + s2.size() + s3.size() + ...);
然而,随之而来的新问题是这样写破坏了可读性且繁琐。
等效替代
仔细一想,要添加哪些字符串,字符串个数都是编译时已知的信息,因此我们可以考虑通过模板元编程
(TMP)中一个技巧:递归模板实例化
(recurring template instantiation)在编译时进行展开,最终等效于上述写法。
我们可以在递归的出口函数中进行预分配,以及最后一个字符串实参的拷贝,然后递归返回时,不断将前面的空间填充即可。
因此需要准备一个size
和index
实参,前者记录至今所有字符串的总长度,而index记录填充的开始位置,等递归返回时就可以知道拷贝空间的尾部位置。
为了兼容std::string
,std::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
namespace util {
namespace detail {
/*
* 利用递归模板实例化在编译时展开模板,从而避免了运行时循环开销
* 由于递归的特性,可以在最后的出口函数进行预分配,
* 并从尾部开始拷贝最后一个字符串
*
* 为兼容C-style,std::string,std::string_view(c++17),重载了三个版本
*/
/*
* 注意,这里必须前向声明,不然递归实例化时,实例化点可能看不到在下面定义的其他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);
}
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;
}
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);
}
template <>
inline void StrAppend_impl<>(std::string &str, size_t &size, size_t &index,
std::string_view const &head)
{
STRAPPEND_STL_STYLE_EXIT;
}
} // 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模式下测的)。
戳-> 测试代码