前一段时间学习 Python
,被这门语言的便利性惊艳到了,比如你可以这样产生一个字
符串:
1 | # output "hello world, PI is 3.14" |
最近学习C++标准库,看到C++11新的变参模板,发现它可以用来实现一个简单的类似
Python
风格字符串格式化函数[^1],语法如下:。
1 | // output "hello world, PI is 3.14" |
本文讲解一个非常简单的实现版本,不处理下面这样的语法:
1 | // output "say you say me" |
如果你需要一个丰富的字符串格式化功能,可以考虑使用cppformat这个库。
具体实现
注意:因为使用了C++11中的新特性,你需要一个C++11编译器,通常你需要在编译的时
候设置 -std=c++11
。本文实现的版本源码可以在我的代码库中找到
基本的实现思路是使用变参模板捕获格式说明符之外的所有参数,然后依次把他们通过
stringstream
拼接在一起,最终返回拼接的结果,函数的声明如下:。
1 | template <typename... Types> |
该函数实现如下:
1 | template <typename... Types> |
该函数声明一个用于构造字符串的 stringstream
,然后通过调用辅助函数
BuildFormatString
完成最终的字符串格式化,之所以这样做的原因是可以把
builder
通过引用传递,从而不用在后续的递归中一次一次的声明临时
stringstream
变量。BuildFormatString
使用递归的方式实现字符串的格式化,具体
实现如下。
1 | void BuildFormatString(std::stringstream& builder, |
其中第一个普通函数(非模板函数)用来终止递归。这种实现方式只要参数支持
stringstream
的输出操作就可以正常的运行。这个版本并没有做过优化,比如它每
一次调用 BuildFormatString
都会产生一个 std::string
类型的临时变量用于存放
格式化说明符 fmt_spec
。可以考虑通过增加一个 pos 参数来解决这个问题,或者是使
用 const char*
替代 std::string
作为格式说明符的类型。此外 builder <<
fmt_spec.substr(0, pos)
中的子串生成可以通过循环取代。事实上直接
使用循环不会给性能带来任何的优化,因为字符的整块处理比循环单字符处理要快很多,
对于优化问题可以参考后面一小节
测试结果
简单的测试代码如下:
1 | int main(int argc, char *argv[]) |
输出结果如下:
1 |
|
如果 ‘{}’ 占位符多于实际提供的参数,占位符会被保留,反之如果参数多于占位符,参 数会被忽略。
优化
2016-03-24更新,之前这个版本没有优化过,后续出于性能调优的考虑,做了一点点小 的改动,记录在此。
使用 ostringstream
替代 stringstrem
上一个版本个人疏忽,使用了 stringstream
作为字符串的builder
,但是实际上我们
只是使用它作为输出缓冲区,所以可以直接使用 ostringstream
替代。
find_first_of
返回值修正
find_first_of
在查找失败的疏忽返回的是 string::npos
通常这个值被定义为 -1
,但是因为它的类型 string::size_type
通常是一个无符号数,所以 npos
变成了能
表示的最大值,所以之前的 pos > fmt_spec.length()
判断才能正常工作,但是这并不
是可移植的方式,正确的写法应该是:
1 | if (pos == std::string::npos) { |
使用 ostringstream::write
去掉前缀子串的创建
之前的版本中为了输入前缀子串使用了 builder << fmt_spec.substr(0, pos);
这样的
语句,它使得每一次前缀输入都需要生成一个前缀子串。我们可以使用 write
成员函数
去掉这个子串的创建,直接写入 string
中的一块内容。
1 | builder.write(fmt_spec.data(), pos); |
使用位置下标去掉后缀子串的创建
在之前处理完一个参数之后,通过递归的方式处理下一个参数:
1 | BuildFormatString(builder, fmt_spec.substr(pos + 2), args...); |
这个递归调用的第二个参数会产生一个临时变量,这个变量其实可以通过位置下标去掉, 做法是更改函数的签名,让他接受一个额外的参数如下:
1 | template <typename T, typename... Types> |
查找的占位符的时候使用带位置信息的版本
1 | auto pos = fmt_spec.find_first_of("{}", idx); |
当然写入前缀的后缀处理的时候也要相应的更改
1 | builder.write(fmt_spec.data() + idx, pos - idx); |
通过上面的这些更改,性能得到了一定的提升,对于下面的测试:
1 | const int kOutLoopCount = 1000; |
没有优化过的版本,平均实践大概是 1294218
优化过的版本的平均时间大概是
1068972
,效果还是不错的。
[^1]: std::string
在实现的时候没有考虑过多态的使用,比如它没有声明虚拟析构函
数,所以不建议继承自 std::string
扩展自己的字符串,你可以使用组合替代继承来
扩展标准库中的字符串,但是使用全局函数的方式更简单方便,所以这里采用这种方式。