(点击上方公众号,可快速关注)
本篇文章主要介绍现代C++字符串格式化的方法。在此之前,回顾了一些老的字符串格式化的方法,并分析各自的优劣。在最后给出了一种提供给老编译器的折中方案,因为新的格式化方法需要C++支持。
字符串格式化是极其常用的功能,这里就不多做介绍,直接进入主题。
在C++20前,标准库也提供了多种的格式化方法,主要有:
C`printf`族函数由C语言标准库引入的一组函数,典型的函数有:
int sprintf(char* buffer, const char* format, ... ); //不推荐使用 int snprintf(char* buffer, std::size_t buf_size, const char* format, ... );
使用方式也比较直观:
int i = 3;
double d = 6.7;
const char* cs = "cs";
std::string ss = "ss";
char str[64] = { 0 };
snprintf(str, sizeof(str)-1, "i=%d, d=%-6.2f, cs=%s, ss=%s", i, d, cs, ss.c_str());
输出:
i=3, d=6.70 , cs=cs, ss=ss
需要注意下,6.70后面有两个空格,因为指定输出6个字符的宽度,而且左对齐,所以右侧空出来2个空格。
这种方式还是比较直观的,但缺点也不少:
非类型安全
参数和转换符类型不符可能导致程序崩溃;参数和转换符个数不一致也可能导致程序崩溃。。。
由于不是类型安全的,编译器没法提前检查类型不一致的情况。估计每个人都犯过这个错:std::string类型的数据没.c_str()。
要求定长数组
因为是在C中引入的,输出参数只能为定长数组,需要事先确定输出的最大长度,使用起来不方便。
重复引用的参数需传入多份
char str[64] = { 0 }; snprintf(str, sizeof(str)-1, "i(%d) == i(%d)", i, i);格式化字符串里使用到两次i,这样参数也要传两次。工作中经常遇到类似的情况,如,组装SQL语句,导致后面参数列表特别长,可读性比较差。
C的方式不太尽如人意,我们看下C++流式的处理代码:
std::stringstream oss; oss << "i=" << i << ", d=" << std::left << std::setw(6) << std::fixed << std::setprecision(2) << d << ", cs=" << cs << ", ss=" << ss;
上面的代码与上例的代码输出完全一样。与snprintf方式相比,这里不用关心数组大小了,std::stringstream会随着内容增加容量动态增长;operator <<重载了常用的类型,也不用担心类型不匹配了,貌似很先进,但使用起来很麻烦。
从参数d的格式化看,这种方式不擅长控制复杂格式,代码简直不是给人看的。一是代码长,二是每种控制符作用范围是不同的,有的作用域整个流,有的仅影响下一个元素,反正我记了好几次,时间一长不用就忘了。
平时在工作中,简单的字符串格式化可以用用这种方法,复杂点的宁愿使用Cprintf族函数。
std::format格式化被人诟病的字符串格式终于在C++20做出了改变,定义在
templatestd::string format(std::string_view fmt, const Args&... args);
fmt是格式字符串,std::string_view类型,不熟悉的同学可以参考之前的文章《性能控的工具箱之string_view》。args是可变的参数列表。
格式字符串的特殊字符是{和},有点类似printf的%,只不过这里它有两个,如果要输出这两个字符,需要分别使用转义序列 {{ 与 }} 。除此之外,正常情况下,以{开始,}结束的部分组成一个可替换域。
每个替换域的格式如下:
引入的 { 字符;
(可选) arg-id ,非负,指代参数的索引,索引从0开始
(可选) 冒号( : )后随格式说明,这里不展开,可参见文章最后给出的参考资料
终止的 } 字符。
上面示例改写如下:
std::format("i={}, d={:<6.2f}, cs={}, ss={}", i, d, cs, ss)
而且它也能解决参数重复的问题:
std::format("i({0}) == i({0})", i);
这里指定了arg-id,都为0,都指向了第一个参数,所以我们就不用把参数写两遍了。这里需要注意的是:要么每个替换域都指定arg-id,要么都不指定,混合使用编译器会报错。
std::format解决了printf族类型不安全的问题,也解决了流式处理可读不高的问题,是C++20推荐的字符串格式化方式。
折中方案C++20刚出不久,截至文章发布时,主流的C++编译器只有msvc支持,需要升级到VS 2019 16.10及以上,gcc、clang目前还不支持,所以这个新的功能大家可能暂时还用不了。下面给出平时我用的折中方案,在编译器没升级之前可以先尝试用这个:
如果是C++11以上编译器,包括C++11:
templatestd::string format(const char *fmt, const T&...t) { const auto len = snprintf(nullptr, 0, fmt, t...); std::string r; r.resize(static_cast (len) + 1); snprintf(&r.front(), len + 1, fmt, t...); // Bad boy r.resize(static_cast (len)); return r; }
否则,使用:
std::string format(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
const auto len = vsnprintf(nullptr, 0, fmt, args);
va_end(args);
std::string r;
r.resize(static_cast(len) + 1);
va_start(args, fmt);
vsnprintf(&r.front(), len + 1, fmt, args);
va_end(args);
r.resize(static_cast(len));
return r;
}
参考资料
常见数据格式:https://en.cppreference.com/w/cpp/utility/format/formatter#Standard_format_specification
日期数据格式:https://en.cppreference.com/w/cpp/chrono/system_clock/formatter#Format_specification
喜欢我的文章,请关注我的公众号。转载请标明出处。



