【现代C++】新的字符串格式化方法
(点击上方公众号,可快速关注)本篇文章主要介绍现代C++字符串格式化的方法。在此之前,回顾了一些老的字符串格式化的方法,并分析各自的优劣。在最后给出了一种提供给老编译器的折中方案,因为新的...
(点击上方公众号,可快速关注)
本篇文章主要介绍现代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的方式不太尽如人意,我们看下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做出了改变,定义在<format>
,它主要借鉴了python、rust等语言的格式化功能,支持的功能很多,甚至包括自定义类型的格式化、日期数据的格式化等,这里仅介绍常用的std::format
函数。
template<class... Args>
std::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:
template<class... T>
std::string format(const char *fmt, const T&...t)
{
const auto len = snprintf(nullptr, 0, fmt, t...);
std::string r;
r.resize(static_cast<size_t>(len) + 1);
snprintf(&r.front(), len + 1, fmt, t...); // Bad boy
r.resize(static_cast<size_t>(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<size_t>(len) + 1);
va_start(args, fmt);
vsnprintf(&r.front(), len + 1, fmt, args);
va_end(args);
r.resize(static_cast<size_t>(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
喜欢我的文章,请关注我的公众号。转载请标明出处。
更多推荐
所有评论(0)